diff --git a/.github/workflows/apiref.yml b/.github/workflows/apiref.yml index fae7485cb6..8d9f8b97f8 100644 --- a/.github/workflows/apiref.yml +++ b/.github/workflows/apiref.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: push: branches: - - "1.10.x" + - "1.11.x" paths: - 'src/**' - 'composer.lock' @@ -45,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 @@ -63,7 +63,7 @@ jobs: node-version: "16" - name: "Download docs" - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: docs path: docs diff --git a/.github/workflows/checksum-phar.yml b/.github/workflows/checksum-phar.yml new file mode 100644 index 0000000000..a7e4238343 --- /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.10.x" + paths: + - 'compiler/**' + - '.github/workflows/checksum-phar.yml' + +env: + COMPOSER_ROOT_VERSION: "1.10.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@v3 + with: + repository: phpstan/phpstan + path: phpstan-dist + ref: 1.10.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@v3 + with: + ref: ${{ steps.info.outputs.commit }} + + - name: "Checkout latest PHAR compiler" + uses: actions/checkout@v3 + 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.10.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.10.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..8452d98693 100644 --- a/.github/workflows/create-tag.yml +++ b/.github/workflows/create-tag.yml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v3 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 70c43f9f25..8f3e5c3392 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -114,6 +114,76 @@ jobs: 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" @@ -144,15 +214,29 @@ 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" diff --git a/.github/workflows/issue-bot.yml b/.github/workflows/issue-bot.yml index 172c3c87dc..251d146474 100644 --- a/.github/workflows/issue-bot.yml +++ b/.github/workflows/issue-bot.yml @@ -3,6 +3,7 @@ name: "Issue bot" on: + workflow_dispatch: pull_request: paths-ignore: - 'compiler/**' @@ -10,14 +11,14 @@ on: - 'changelog-generator/**' push: branches: - - "1.10.x" + - "1.11.x" paths-ignore: - 'compiler/**' - 'apigen/**' - 'changelog-generator/**' env: - COMPOSER_ROOT_VERSION: "1.10.x-dev" + 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 @@ -50,9 +51,9 @@ jobs: uses: actions/cache@v3 with: path: ./issue-bot/tmp - key: "issue-bot-download-v4-${{ github.run_id }}" + key: "issue-bot-download-v6-${{ github.run_id }}" restore-keys: | - issue-bot-download-v4- + issue-bot-download-v6- - name: "Download data" working-directory: "issue-bot" @@ -62,12 +63,12 @@ jobs: run: echo "matrix=$(./console.php download)" >> $GITHUB_OUTPUT - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: playground-cache path: issue-bot/tmp/playgroundCache.tmp - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: issue-cache path: issue-bot/tmp/issueCache.tmp @@ -77,7 +78,6 @@ jobs: needs: download runs-on: "ubuntu-latest" - timeout-minutes: 5 strategy: fail-fast: false @@ -100,18 +100,23 @@ jobs: working-directory: "issue-bot" run: "composer install --no-interaction --no-progress" - - uses: actions/download-artifact@v3 + - uses: Wandalen/wretry.action@v1.3.0 with: - name: playground-cache - path: issue-bot/tmp + 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@v3 + - uses: actions/upload-artifact@v4 with: - name: results + name: results-${{ matrix.phpVersion }}-${{ matrix.chunkNumber }} path: issue-bot/tmp/results-${{ matrix.phpVersion }}-*.tmp evaluate: @@ -134,19 +139,20 @@ jobs: working-directory: "issue-bot" run: "composer install --no-interaction --no-progress" - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: playground-cache path: issue-bot/tmp - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: issue-cache path: issue-bot/tmp - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: - name: results + pattern: results-* + merge-multiple: true path: issue-bot/tmp - name: "Evaluate results - pull request" @@ -156,7 +162,7 @@ jobs: - name: "Evaluate results - push" working-directory: "issue-bot" - if: "github.repository_owner == 'phpstan' && github.ref == 'refs/heads/1.10.x'" + 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 }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f16461048f..2567d55af5 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -31,6 +31,7 @@ jobs: - "8.0" - "8.1" - "8.2" + - "8.3" steps: - name: "Checkout" @@ -48,38 +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 "sha=$(php build/rector-cache-files-hash.php)" >> $GITHUB_OUTPUT - - - 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-v3-lint-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ matrix.php-version }}-${{ steps.rector-cache-key.outputs.sha }}" - restore-keys: | - rector-v3-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" @@ -131,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@v3 + + - 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 155effc429..9ece75e5d4 100644 --- a/.github/workflows/merge-maintained-branch.yml +++ b/.github/workflows/merge-maintained-branch.yml @@ -5,11 +5,12 @@ name: Merge maintained branch on: push: branches: - - "1.9.x" + - "1.10.x" jobs: merge: name: Merge branch + if: github.repository_owner == 'phpstan' runs-on: ubuntu-latest steps: - name: "Checkout" @@ -19,5 +20,5 @@ jobs: with: github_token: "${{ secrets.PHPSTAN_BOT_TOKEN }}" source_ref: ${{ github.ref }} - target_branch: '1.10.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 14bea0ed78..ef0f975e49 100644 --- a/.github/workflows/phar.yml +++ b/.github/workflows/phar.yml @@ -51,18 +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 "sha=$(php build/rector-cache-files-hash.php)" >> $GITHUB_OUTPUT - - - name: "Rector downgrade cache" - uses: actions/cache@v3 - with: - path: ./tmp/rectorCache.php - key: "rector-v3-phar-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}-${{ steps.rector-cache-key.outputs.sha }}" - restore-keys: | - rector-v3-phar-${{ hashFiles('composer.lock', 'build/rector-downgrade.php') }}- - - name: "Prepare for PHAR compilation" working-directory: "compiler" run: "php bin/prepare" @@ -71,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 @@ -108,7 +96,7 @@ jobs: id: "checksum" 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 @@ -129,7 +117,7 @@ jobs: needs: compiler-tests uses: phpstan/phpstan/.github/workflows/extension-tests.yml@1.10.x with: - ref: 1.1O.x + ref: 1.10.x phar-checksum: ${{needs.compiler-tests.outputs.checksum}} other-tests: @@ -147,17 +135,23 @@ jobs: 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@v5 + 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 with: repository: phpstan/phpstan path: phpstan-dist - token: ${{ secrets.PAT }} + token: ${{ secrets.PHPSTAN_BOT_TOKEN }} ref: 1.10.x - name: "Get previous pushed dist commit" @@ -196,7 +190,7 @@ jobs: fi - name: "Download phpstan.phar" - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: phar-file @@ -215,20 +209,14 @@ 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@v1 + uses: baptiste0928/cargo-install@v2 with: crate: lucky_commit args: --no-default-features @@ -239,10 +227,10 @@ jobs: env: INPUT_LOG: ${{ steps.git-log.outputs.log }} run: | - git config --global user.name "Ondrej Mirtes" - git config --global user.email "ondrej@mirtes.cz" + 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 "Ondrej Mirtes " + 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 @@ -250,9 +238,9 @@ jobs: if: "startsWith(github.ref, 'refs/tags/')" uses: stefanzweifel/git-auto-commit-action@v4 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-marked-as-ready.yml b/.github/workflows/pr-marked-as-ready.yml index c69e288cd4..7bdbc585c9 100644 --- a/.github/workflows/pr-marked-as-ready.yml +++ b/.github/workflows/pr-marked-as-ready.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Comment PR - uses: peter-evans/create-or-update-comment@v2 + uses: peter-evans/create-or-update-comment@v3 with: body: "This pull request has been marked as ready for review." token: ${{ secrets.PHPSTAN_BOT_TOKEN }} diff --git a/.github/workflows/pr-title-edited.yml b/.github/workflows/pr-title-edited.yml deleted file mode 100644 index 7c893a489c..0000000000 --- a/.github/workflows/pr-title-edited.yml +++ /dev/null @@ -1,29 +0,0 @@ -# https://help.github.com/en/categories/automating-your-workflow-with-github-actions - -name: "Pull request title edited" - -on: - pull_request_target: - types: - - edited - -concurrency: - group: pr-title-edited-${{ github.head_ref }} # will be canceled on subsequent pushes in pull requests - -jobs: - revert-title: - name: "Revert title" - runs-on: 'ubuntu-latest' - - steps: - - run: echo "${{ toJSON(github.event.changes) }}" - - uses: octokit/request-action@v2.x - if: github.event.sender.login != 'phpstan-bot' && github.event.changes.title.from != '' - with: - route: PATCH /repos/{owner}/{repo}/pulls/{pull_number} - owner: phpstan - repo: phpstan-src - pull_number: ${{ github.event.number }} - title: ${{ github.event.changes.title.from }} - env: - GITHUB_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} diff --git a/.github/workflows/reflection-golden-test.yml b/.github/workflows/reflection-golden-test.yml new file mode 100644 index 0000000000..807e52c498 --- /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.10.x" + paths-ignore: + - 'compiler/**' + - 'apigen/**' + - 'changelog-generator/**' + - 'issue-bot/**' + +env: + COMPOSER_ROOT_VERSION: "1.10.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@v3 + + - 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@v1.3.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@v3 + 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@v3 + + - 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 09d8b1998a..625fbb8034 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -37,6 +37,7 @@ jobs: - "8.0" - "8.1" - "8.2" + - "8.3" operating-system: [ubuntu-latest, windows-latest] steps: @@ -54,41 +55,15 @@ 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 "sha=$(php build/rector-cache-files-hash.php)" >> $GITHUB_OUTPUT - - - 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-v3-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-v3-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 }}" - ini-file: development - 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' @@ -109,6 +84,7 @@ jobs: php-version: - "8.1" - "8.2" + - "8.3" steps: - name: "Checkout" @@ -129,7 +105,9 @@ jobs: uses: actions/cache@v3 with: path: ./tmp - key: "result-cache-v4-${{ matrix.php-version }}" + key: "result-cache-v12-${{ matrix.php-version }}-${{ github.run_id }}" + restore-keys: | + result-cache-v12-${{ 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" @@ -171,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@v3 + + - 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 9a6ebe3f31..6254c43006 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -40,6 +40,7 @@ jobs: - "8.0" - "8.1" - "8.2" + - "8.3" operating-system: [ ubuntu-latest, windows-latest ] steps: @@ -59,43 +60,10 @@ 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 "sha=$(php build/rector-cache-files-hash.php)" >> $GITHUB_OUTPUT - - - 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-v3-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-v3-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-file: development - ini-values: memory_limit=2G + run: "vendor/bin/simple-downgrade downgrade -c build/downgrade.php ${{ matrix.php-version }}" - name: "Tests" run: "make tests" @@ -130,11 +98,46 @@ 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@v3 + + - 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 @@ -143,7 +146,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "8.1" + php-version: "8.3" tools: pecl extensions: ds,mbstring ini-file: development @@ -153,7 +156,7 @@ jobs: run: "composer install --no-interaction --no-progress" - name: "Tests" - run: "make tests-levels" + run: "${{ matrix.script }}" tests-old-phpunit: name: "Tests with old PHPUnit" @@ -184,39 +187,13 @@ jobs: - 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 "sha=$(php build/rector-cache-files-hash.php)" >> $GITHUB_OUTPUT - - - name: "Rector downgrade cache" - uses: actions/cache@v3 - with: - path: ./tmp/rectorCache.php - key: "rector-v3-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-v3-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-file: development - 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:^8.5.31 brianium/paratest:^4.0 --update-with-dependencies --ignore-platform-reqs" diff --git a/.github/workflows/update-phpstorm-stubs.yml b/.github/workflows/update-phpstorm-stubs.yml index c7845d24d7..7bde36e651 100644 --- a/.github/workflows/update-phpstorm-stubs.yml +++ b/.github/workflows/update-phpstorm-stubs.yml @@ -16,7 +16,7 @@ jobs: - name: "Checkout" uses: actions/checkout@v3 with: - ref: 1.9.x + ref: 1.10.x fetch-depth: '0' token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - name: "Install PHP" @@ -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@v5 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..2d559bd7fc 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 \ @@ -37,6 +41,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 +51,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 +90,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/bin/functionMetadata_original.php b/bin/functionMetadata_original.php index 81fcb58663..6e71398632 100644 --- a/bin/functionMetadata_original.php +++ b/bin/functionMetadata_original.php @@ -70,6 +70,9 @@ '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], @@ -87,9 +90,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], @@ -99,6 +104,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..404e742aa6 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,8 @@ public function enterNode(Node $node) 'rand', 'random_bytes', 'random_int', + 'connection_aborted', + 'connection_status', ], 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.lock b/build-cs/composer.lock index 6417c4ebba..dab6b09410 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -9,16 +9,16 @@ "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,9 +70,9 @@ ], "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", @@ -154,16 +154,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.15.3", + "version": "1.20.4", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "61800f71a5526081d1b5633766aa88341f1ade76" + "reference": "7d568c87a9df9c5f7e8b5f075fc469aa8cb0a4cd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/61800f71a5526081d1b5633766aa88341f1ade76", - "reference": "61800f71a5526081d1b5633766aa88341f1ade76", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/7d568c87a9df9c5f7e8b5f075fc469aa8cb0a4cd", + "reference": "7d568c87a9df9c5f7e8b5f075fc469aa8cb0a4cd", "shasum": "" }, "require": { @@ -193,38 +193,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.15.3" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.20.4" }, - "time": "2022-12-20T20:56:55+00:00" + "time": "2023-05-02T09:19:37+00:00" }, { "name": "slevomat/coding-standard", - "version": "8.8.0", + "version": "8.11.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "59e25146a4ef0a7b194c5bc55b32dd414345db89" + "reference": "af87461316b257e46e15bb041dca6fca3796d822" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/59e25146a4ef0a7b194c5bc55b32dd414345db89", - "reference": "59e25146a4ef0a7b194c5bc55b32dd414345db89", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/af87461316b257e46e15bb041dca6fca3796d822", + "reference": "af87461316b257e46e15bb041dca6fca3796d822", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", "php": "^7.2 || ^8.0", - "phpstan/phpdoc-parser": ">=1.15.2 <1.16.0", + "phpstan/phpdoc-parser": ">=1.20.0 <1.21.0", "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.9.6", - "phpstan/phpstan-deprecation-rules": "1.1.1", - "phpstan/phpstan-phpunit": "1.0.0|1.3.3", - "phpstan/phpstan-strict-rules": "1.4.4", - "phpunit/phpunit": "7.5.20|8.5.21|9.5.27" + "phpstan/phpstan": "1.10.14", + "phpstan/phpstan-deprecation-rules": "1.1.3", + "phpstan/phpstan-phpunit": "1.3.11", + "phpstan/phpstan-strict-rules": "1.5.1", + "phpunit/phpunit": "7.5.20|8.5.21|9.6.6|10.1.1" }, "type": "phpcodesniffer-standard", "extra": { @@ -234,7 +234,7 @@ }, "autoload": { "psr-4": { - "SlevomatCodingStandard\\": "SlevomatCodingStandard" + "SlevomatCodingStandard\\": "SlevomatCodingStandard/" } }, "notification-url": "https://packagist.org/downloads/", @@ -248,7 +248,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.8.0" + "source": "https://github.com/slevomat/coding-standard/tree/8.11.1" }, "funding": [ { @@ -260,20 +260,20 @@ "type": "tidelift" } ], - "time": "2023-01-09T10:46:13+00:00" + "time": "2023-04-24T08:19:01+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": { @@ -309,14 +309,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": [], 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 fb6f7a8bfa..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->isTrue()->yes()) { - $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 ac3d3e09c7..3e0d07184d 100644 --- a/build/baseline-8.0.neon +++ b/build/baseline-8.0.neon @@ -6,18 +6,28 @@ parameters: path: ../src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php - - message: "#^Strict comparison using \\=\\=\\= between list 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\\.$#" 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/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 list and false will always evaluate to false\\.$#" - count: 2 + count: 1 path: ../src/Type/Php/StrSplitFunctionReturnTypeExtension.php - 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 dbbf8331fe..04a18bb846 100644 --- a/build/enum-adapter-errors.neon +++ b/build/enum-adapter-errors.neon @@ -1,53 +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 getStartLine\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Rules/Classes/DuplicateClassDeclarationRule.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 - @@ -85,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 @@ -102,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 @@ -182,7 +112,7 @@ parameters: - message: "#^Class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum not found\\.$#" - count: 5 + count: 4 path: ../src/Reflection/ClassReflection.php - @@ -210,162 +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 getMethods\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Rules/Methods/MissingMagicSerializationMethodsRule.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 - - - - message: "#^Call to method getProperties\\(\\) on an unknown class PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum\\.$#" - count: 1 - path: ../src/Node/ClassStatementsGatherer.php diff --git a/build/enums.neon b/build/enums.neon index 355e5ce838..3ec87ab42e 100644 --- a/build/enums.neon +++ b/build/enums.neon @@ -2,6 +2,7 @@ parameters: excludePaths: - ../tests/PHPStan/Fixture/TestEnum.php - ../tests/PHPStan/Fixture/AnotherTestEnum.php + - ../tests/PHPStan/Fixture/ManyCasesTestEnum.php ignoreErrors: - diff --git a/build/more-enum-adapter-errors.neon b/build/more-enum-adapter-errors.neon index fecb2498db..69882bcfc5 100644 --- a/build/more-enum-adapter-errors.neon +++ b/build/more-enum-adapter-errors.neon @@ -18,3 +18,8 @@ parameters: 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 a07f33f27b..4c1ee7b1a6 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 @@ -89,6 +88,7 @@ parameters: 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: 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-pre-php-7.neon b/build/spl-autoload-functions-pre-php-7.neon index a34657f54b..abafcfbf08 100644 --- a/build/spl-autoload-functions-pre-php-7.neon +++ b/build/spl-autoload-functions-pre-php-7.neon @@ -4,3 +4,7 @@ parameters: 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 4942dd02d5..a9ba4c0d67 100644 --- a/changelog-generator/composer.lock +++ b/changelog-generator/composer.lock @@ -463,16 +463,16 @@ }, { "name": "knplabs/github-api", - "version": "v3.9.0", + "version": "v3.11.0", "source": { "type": "git", "url": "https://github.com/KnpLabs/php-github-api.git", - "reference": "665ba275dbf36f9e9ef78876f27ca87796e3599c" + "reference": "c68b874ac3267c3cc0544b726dbb4e49a72a9920" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/KnpLabs/php-github-api/zipball/665ba275dbf36f9e9ef78876f27ca87796e3599c", - "reference": "665ba275dbf36f9e9ef78876f27ca87796e3599c", + "url": "https://api.github.com/repos/KnpLabs/php-github-api/zipball/c68b874ac3267c3cc0544b726dbb4e49a72a9920", + "reference": "c68b874ac3267c3cc0544b726dbb4e49a72a9920", "shasum": "" }, "require": { @@ -506,7 +506,7 @@ "extra": { "branch-alias": { "dev-2.x": "2.20.x-dev", - "dev-master": "3.9.x-dev" + "dev-master": "3.10.x-dev" } }, "autoload": { @@ -539,7 +539,7 @@ ], "support": { "issues": "https://github.com/KnpLabs/php-github-api/issues", - "source": "https://github.com/KnpLabs/php-github-api/tree/v3.9.0" + "source": "https://github.com/KnpLabs/php-github-api/tree/v3.11.0" }, "funding": [ { @@ -547,7 +547,7 @@ "type": "github" } ], - "time": "2022-10-24T12:42:09+00:00" + "time": "2023-03-10T11:40:14+00:00" }, { "name": "php-http/cache-plugin", @@ -685,38 +685,44 @@ }, { "name": "php-http/discovery", - "version": "1.14.3", + "version": "1.15.3", "source": { "type": "git", "url": "https://github.com/php-http/discovery.git", - "reference": "31d8ee46d0215108df16a8527c7438e96a4d7735" + "reference": "3ccd28dd9fb34b52db946abea1b538568e34eae8" }, "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/3ccd28dd9fb34b52db946abea1b538568e34eae8", + "reference": "3ccd28dd9fb34b52db946abea1b538568e34eae8", "shasum": "" }, "require": { + "composer-plugin-api": "^1.0|^2.0", "php": "^7.1 || ^8.0" }, "conflict": { "nyholm/psr7": "<1.0" }, + "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": { @@ -733,7 +739,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", @@ -742,13 +748,14 @@ "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.15.3" }, - "time": "2022-07-11T14:04:40+00:00" + "time": "2023-03-31T14:40:37+00:00" }, { "name": "php-http/httplug", @@ -1367,16 +1374,16 @@ }, { "name": "symfony/console", - "version": "v6.2.5", + "version": "v6.2.8", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "3e294254f2191762c1d137aed4b94e966965e985" + "reference": "3582d68a64a86ec25240aaa521ec8bc2342b369b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/3e294254f2191762c1d137aed4b94e966965e985", - "reference": "3e294254f2191762c1d137aed4b94e966965e985", + "url": "https://api.github.com/repos/symfony/console/zipball/3582d68a64a86ec25240aaa521ec8bc2342b369b", + "reference": "3582d68a64a86ec25240aaa521ec8bc2342b369b", "shasum": "" }, "require": { @@ -1438,12 +1445,12 @@ "homepage": "https://symfony.com/", "keywords": [ "cli", - "command line", + "command-line", "console", "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.2.5" + "source": "https://github.com/symfony/console/tree/v6.2.8" }, "funding": [ { @@ -1459,20 +1466,20 @@ "type": "tidelift" } ], - "time": "2023-01-01T08:38:09+00:00" + "time": "2023-03-29T21:42:15+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.2.0", + "version": "v3.2.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "1ee04c65529dea5d8744774d474e7cbd2f1206d3" + "reference": "e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/1ee04c65529dea5d8744774d474e7cbd2f1206d3", - "reference": "1ee04c65529dea5d8744774d474e7cbd2f1206d3", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e", + "reference": "e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e", "shasum": "" }, "require": { @@ -1510,7 +1517,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.2.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.2.1" }, "funding": [ { @@ -1526,20 +1533,20 @@ "type": "tidelift" } ], - "time": "2022-11-25T10:21:52+00:00" + "time": "2023-03-01T10:25:55+00:00" }, { "name": "symfony/options-resolver", - "version": "v6.1.0", + "version": "v6.2.7", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "a3016f5442e28386ded73c43a32a5b68586dd1c4" + "reference": "aa0e85b53bbb2b4951960efd61d295907eacd629" }, "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/aa0e85b53bbb2b4951960efd61d295907eacd629", + "reference": "aa0e85b53bbb2b4951960efd61d295907eacd629", "shasum": "" }, "require": { @@ -1577,7 +1584,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v6.1.0" + "source": "https://github.com/symfony/options-resolver/tree/v6.2.7" }, "funding": [ { @@ -1593,7 +1600,7 @@ "type": "tidelift" } ], - "time": "2022-02-25T11:15:52+00:00" + "time": "2023-02-14T08:44:56+00:00" }, { "name": "symfony/polyfill-ctype", @@ -2010,16 +2017,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.2.0", + "version": "v3.2.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "aac98028c69df04ee77eb69b96b86ee51fbf4b75" + "reference": "a8c9cedf55f314f3a186041d19537303766df09a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/aac98028c69df04ee77eb69b96b86ee51fbf4b75", - "reference": "aac98028c69df04ee77eb69b96b86ee51fbf4b75", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/a8c9cedf55f314f3a186041d19537303766df09a", + "reference": "a8c9cedf55f314f3a186041d19537303766df09a", "shasum": "" }, "require": { @@ -2075,7 +2082,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.2.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.2.1" }, "funding": [ { @@ -2091,20 +2098,20 @@ "type": "tidelift" } ], - "time": "2022-11-25T10:21:52+00:00" + "time": "2023-03-01T10:32:47+00:00" }, { "name": "symfony/string", - "version": "v6.2.5", + "version": "v6.2.8", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "b2dac0fa27b1ac0f9c0c0b23b43977f12308d0b0" + "reference": "193e83bbd6617d6b2151c37fff10fa7168ebddef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/b2dac0fa27b1ac0f9c0c0b23b43977f12308d0b0", - "reference": "b2dac0fa27b1ac0f9c0c0b23b43977f12308d0b0", + "url": "https://api.github.com/repos/symfony/string/zipball/193e83bbd6617d6b2151c37fff10fa7168ebddef", + "reference": "193e83bbd6617d6b2151c37fff10fa7168ebddef", "shasum": "" }, "require": { @@ -2161,7 +2168,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.2.5" + "source": "https://github.com/symfony/string/tree/v6.2.8" }, "funding": [ { @@ -2177,7 +2184,7 @@ "type": "tidelift" } ], - "time": "2023-01-01T08:38:09+00:00" + "time": "2023-03-20T16:06:02+00:00" } ], "packages-dev": [], diff --git a/changelog-generator/phpstan.neon b/changelog-generator/phpstan.neon index 65e29257ab..b90066359a 100644 --- a/changelog-generator/phpstan.neon +++ b/changelog-generator/phpstan.neon @@ -1,7 +1,6 @@ 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 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 f5e76e4c0b..fb2b67034b 100644 --- a/compiler/composer.lock +++ b/compiler/composer.lock @@ -1026,16 +1026,16 @@ }, { "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.2", + "version": "v4.15.4", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc" + "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc", - "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6bb5176bc4af8bcb7d926f88718db9b96a2d4290", + "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290", "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.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.4" }, - "time": "2022-11-12T15:38:23+00:00" + "time": "2023-03-05T19:49:14+00:00" }, { "name": "phar-io/manifest", @@ -1252,16 +1252,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.9.4", + "version": "1.10.8", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "d03bccee595e2146b7c9d174486b84f4dc61b0f2" + "reference": "0166aef76e066f0dd2adc2799bdadfa1635711e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/d03bccee595e2146b7c9d174486b84f4dc61b0f2", - "reference": "d03bccee595e2146b7c9d174486b84f4dc61b0f2", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0166aef76e066f0dd2adc2799bdadfa1635711e9", + "reference": "0166aef76e066f0dd2adc2799bdadfa1635711e9", "shasum": "" }, "require": { @@ -1290,8 +1290,11 @@ "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.9.4" + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" }, "funding": [ { @@ -1307,25 +1310,25 @@ "type": "tidelift" } ], - "time": "2022-12-17T13:33:52+00:00" + "time": "2023-03-24T10:28:16+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "1.3.3", + "version": "1.3.11", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "54a24bd23e9e80ee918cdc24f909d376c2e273f7" + "reference": "9e1b9de6d260461f6e99b6a8f2dbb0bbb98b579c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/54a24bd23e9e80ee918cdc24f909d376c2e273f7", - "reference": "54a24bd23e9e80ee918cdc24f909d376c2e273f7", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/9e1b9de6d260461f6e99b6a8f2dbb0bbb98b579c", + "reference": "9e1b9de6d260461f6e99b6a8f2dbb0bbb98b579c", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.9.3" + "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.3.3" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.3.11" }, - "time": "2022-12-21T15:25:00+00:00" + "time": "2023-03-25T19:42:13+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.23", + "version": "9.2.26", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c" + "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c", - "reference": "9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", + "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", "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,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.23" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.26" }, "funding": [ { @@ -1436,7 +1439,7 @@ "type": "github" } ], - "time": "2022-12-28T12:41:10+00:00" + "time": "2023-03-06T12:58:08+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1681,16 +1684,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.28", + "version": "9.6.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "954ca3113a03bf780d22f07bf055d883ee04b65e" + "reference": "c993f0d3b0489ffc42ee2fe0bd645af1538a63b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/954ca3113a03bf780d22f07bf055d883ee04b65e", - "reference": "954ca3113a03bf780d22f07bf055d883ee04b65e", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c993f0d3b0489ffc42ee2fe0bd645af1538a63b2", + "reference": "c993f0d3b0489ffc42ee2fe0bd645af1538a63b2", "shasum": "" }, "require": { @@ -1723,8 +1726,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 +1735,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.5-dev" + "dev-master": "9.6-dev" } }, "autoload": { @@ -1763,7 +1766,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.28" + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.7" }, "funding": [ { @@ -1779,7 +1783,7 @@ "type": "tidelift" } ], - "time": "2023-01-14T12:32:24+00:00" + "time": "2023-04-14T08:58:40+00:00" }, { "name": "sebastian/cli-parser", @@ -2081,16 +2085,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 +2139,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 +2147,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 +2202,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 +2210,7 @@ "type": "github" } ], - "time": "2022-04-03T09:37:03+00:00" + "time": "2023-02-03T06:03:51+00:00" }, { "name": "sebastian/exporter", @@ -2520,16 +2524,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 +2572,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 +2583,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 +2642,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 +2686,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 +2694,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 a289d91ced..0022ade978 100644 --- a/composer.json +++ b/composer.json @@ -13,21 +13,23 @@ "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#c1d72aec3a5fbdb3d4568076d03644c9439b78cb", + "jetbrains/phpstorm-stubs": "dev-master#b0e68128846d14ecc886fb53479e39db5453f7f3", "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.3", + "nikic/php-parser": "^4.17.1", "ondram/ci-detector": "^3.4.0", - "ondrejmirtes/better-reflection": "6.4.0", - "phpstan/php-8-stubs": "0.3.55", - "phpstan/phpdoc-parser": "1.16.0", + "ondrejmirtes/better-reflection": "6.25.0.7", + "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", @@ -38,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" @@ -51,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-nette": "^1.0", - "phpstan/phpstan-php-parser": "^1.1", "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-strict-rules": "^1.5", + "phpstan/phpstan-strict-rules": "^1.5.1", "phpunit/phpunit": "^9.5.4", - "rector/rector": "^0.15", - "vaimo/composer-patches": "^4.22" + "shipmonk/composer-dependency-analyser": "^1.5", + "shipmonk/name-collision-detector": "^2.0" }, "config": { "platform": { @@ -69,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 7139932ad9..325c0a375f 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": "2a2f789eec6d837c88c35bebb7ab4627", + "content-hash": "32eff230a57b8a7ecf43325017abcdca", "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.4", + "version": "1.3.7", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "69098eca243998b53eed7a48d82dedd28b447cd5" + "reference": "76e46335014860eec1aa5a724799a00a2e47cc85" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/69098eca243998b53eed7a48d82dedd28b447cd5", - "reference": "69098eca243998b53eed7a48d82dedd28b447cd5", + "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.4" + "source": "https://github.com/composer/ca-bundle/tree/1.3.7" }, "funding": [ { @@ -144,7 +144,7 @@ "type": "tidelift" } ], - "time": "2022-10-12T12:08:29+00:00" + "time": "2023-08-30T09:31:38+00:00" }, { "name": "composer/pcre", @@ -567,12 +567,12 @@ } }, "autoload": { - "psr-4": { - "Hoa\\Consistency\\": "." - }, "files": [ "Prelude.php" - ] + ], + "psr-4": { + "Hoa\\Consistency\\": "." + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -974,12 +974,12 @@ } }, "autoload": { - "psr-4": { - "Hoa\\Protocol\\": "." - }, "files": [ "Wrapper.php" - ] + ], + "psr-4": { + "Hoa\\Protocol\\": "." + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1353,20 +1353,19 @@ "source": { "type": "git", "url": "https://github.com/JetBrains/phpstorm-stubs.git", - "reference": "c1d72aec3a5fbdb3d4568076d03644c9439b78cb" + "reference": "b0e68128846d14ecc886fb53479e39db5453f7f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/c1d72aec3a5fbdb3d4568076d03644c9439b78cb", - "reference": "c1d72aec3a5fbdb3d4568076d03644c9439b78cb", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/b0e68128846d14ecc886fb53479e39db5453f7f3", + "reference": "b0e68128846d14ecc886fb53479e39db5453f7f3", "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", @@ -1394,26 +1393,26 @@ "support": { "source": "https://github.com/JetBrains/phpstorm-stubs/tree/master" }, - "time": "2023-01-30T21:37:54+00:00" + "time": "2024-04-01T17:45:10+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" @@ -1473,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": { @@ -1548,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": { @@ -1581,7 +1577,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } }, "autoload": { @@ -1615,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", @@ -1824,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", @@ -1880,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", @@ -1971,21 +1967,21 @@ }, { "name": "nikic/php-parser", - "version": "v4.15.3", + "version": "v4.19.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039" + "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/570e980a201d8ed0236b0a62ddf2c9cbb2034039", - "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039", + "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", @@ -2021,9 +2017,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.3" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.1" }, - "time": "2023-01-16T22:05:37+00:00" + "time": "2024-03-17T08:10:35+00:00" }, { "name": "ondram/ci-detector", @@ -2099,34 +2095,34 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "6.4.0", + "version": "6.25.0.7", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "4366598ca217a1a47bfd429f30d8d6e59b5b46e7" + "reference": "9f33397c409cfc29e90ece9b7e07e2550f5a3dea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/4366598ca217a1a47bfd429f30d8d6e59b5b46e7", - "reference": "4366598ca217a1a47bfd429f30d8d6e59b5b46e7", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/9f33397c409cfc29e90ece9b7e07e2550f5a3dea", + "reference": "9f33397c409cfc29e90ece9b7e07e2550f5a3dea", "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.10", - "phpstan/phpstan-phpunit": "^1.2.2", - "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" @@ -2165,22 +2161,22 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.4.0" + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.25.0.7" }, - "time": "2022-12-01T14:49:02+00:00" + "time": "2024-04-02T08:30:06+00:00" }, { "name": "phpstan/php-8-stubs", - "version": "0.3.55", + "version": "0.3.84", "source": { "type": "git", "url": "https://github.com/phpstan/php-8-stubs.git", - "reference": "6d7d370d00fcb0a4e7332b51ccc86c699181994a" + "reference": "d713e9c3f6f8223d323efe9558b477ae92e989df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/6d7d370d00fcb0a4e7332b51ccc86c699181994a", - "reference": "6d7d370d00fcb0a4e7332b51ccc86c699181994a", + "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/d713e9c3f6f8223d323efe9558b477ae92e989df", + "reference": "d713e9c3f6f8223d323efe9558b477ae92e989df", "shasum": "" }, "type": "library", @@ -2197,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.55" + "source": "https://github.com/phpstan/php-8-stubs/tree/0.3.84" }, - "time": "2023-01-25T00:16:00+00:00" + "time": "2023-12-30T11:29:15+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.16.0", + "version": "1.28.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "57090cfccbfaa639e703c007486d605a6e80f56d" + "reference": "cd06d6b1a1b3c75b0b83f97577869fd85a3cd4fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/57090cfccbfaa639e703c007486d605a6e80f56d", - "reference": "57090cfccbfaa639e703c007486d605a6e80f56d", + "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", @@ -2242,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.16.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.28.0" }, - "time": "2023-01-29T14:41:23+00:00" + "time": "2024-04-03T18:51:33+00:00" }, { "name": "psr/container", @@ -2296,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": { @@ -2343,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", @@ -2474,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": { @@ -2491,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": { @@ -2534,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", @@ -2709,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/", @@ -2771,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": { @@ -2806,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" @@ -2815,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/", @@ -2867,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": { @@ -2943,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", @@ -3352,16 +3249,16 @@ }, { "name": "symfony/console", - "version": "v5.4.16", + "version": "v5.4.28", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "8e9b9c8dfb33af6057c94e1b44846bee700dc5ef" + "reference": "f4f71842f24c2023b91237c72a365306f3c58827" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/8e9b9c8dfb33af6057c94e1b44846bee700dc5ef", - "reference": "8e9b9c8dfb33af6057c94e1b44846bee700dc5ef", + "url": "https://api.github.com/repos/symfony/console/zipball/f4f71842f24c2023b91237c72a365306f3c58827", + "reference": "f4f71842f24c2023b91237c72a365306f3c58827", "shasum": "" }, "require": { @@ -3426,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.16" + "source": "https://github.com/symfony/console/tree/v5.4.28" }, "funding": [ { @@ -3447,20 +3344,20 @@ "type": "tidelift" } ], - "time": "2022-11-25T14:09:27+00:00" + "time": "2023-08-07T06:12:30+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.2.0", + "version": "v3.3.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "1ee04c65529dea5d8744774d474e7cbd2f1206d3" + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/1ee04c65529dea5d8744774d474e7cbd2f1206d3", - "reference": "1ee04c65529dea5d8744774d474e7cbd2f1206d3", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", "shasum": "" }, "require": { @@ -3469,7 +3366,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.3-dev" + "dev-main": "3.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -3498,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.2.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.3.0" }, "funding": [ { @@ -3514,20 +3411,20 @@ "type": "tidelift" } ], - "time": "2022-11-25T10:21: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": { @@ -3561,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": [ { @@ -3577,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.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", "shasum": "" }, "require": { @@ -3605,7 +3502,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3643,7 +3540,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" }, "funding": [ { @@ -3659,20 +3556,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354" + "reference": "875e90aeea2777b6f135677f618529449334a612" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", + "reference": "875e90aeea2777b6f135677f618529449334a612", "shasum": "" }, "require": { @@ -3684,7 +3581,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3724,7 +3621,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0" }, "funding": [ { @@ -3740,20 +3637,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", "shasum": "" }, "require": { @@ -3765,7 +3662,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3808,7 +3705,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0" }, "funding": [ { @@ -3824,20 +3721,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.27.0", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" + "reference": "42292d99c55abe617799667f454222c54c60e229" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", + "reference": "42292d99c55abe617799667f454222c54c60e229", "shasum": "" }, "require": { @@ -3852,7 +3749,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3891,7 +3788,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" }, "funding": [ { @@ -3907,20 +3804,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-07-28T09:04:16+00:00" }, { - "name": "symfony/polyfill-php72", - "version": "v1.27.0", + "name": "symfony/polyfill-php73", + "version": "v1.28.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "869329b1e9894268a8a61dabb69153029b7a8c97" + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/869329b1e9894268a8a61dabb69153029b7a8c97", - "reference": "869329b1e9894268a8a61dabb69153029b7a8c97", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fe2f306d1d9d346a7fee353d0d5012e401e984b5", + "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5", "shasum": "" }, "require": { @@ -3929,7 +3826,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3941,8 +3838,11 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php72\\": "" - } + "Symfony\\Polyfill\\Php73\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3958,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", @@ -3967,7 +3867,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.28.0" }, "funding": [ { @@ -3983,20 +3883,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { - "name": "symfony/polyfill-php73", - "version": "v1.27.0", + "name": "symfony/polyfill-php74", + "version": "v1.28.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9" + "url": "https://github.com/symfony/polyfill-php74.git", + "reference": "8b755b41a155c89f1af29cc33305538499fa05ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/9e8ecb5f92152187c4799efd3c96b78ccab18ff9", - "reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9", + "url": "https://api.github.com/repos/symfony/polyfill-php74/zipball/8b755b41a155c89f1af29cc33305538499fa05ea", + "reference": "8b755b41a155c89f1af29cc33305538499fa05ea", "shasum": "" }, "require": { @@ -4005,7 +3905,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4017,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" @@ -4037,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", @@ -4046,7 +3947,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php74/tree/v1.28.0" }, "funding": [ { @@ -4062,20 +3963,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { - "name": "symfony/polyfill-php74", - "version": "v1.27.0", + "name": "symfony/polyfill-php80", + "version": "v1.28.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php74.git", - "reference": "aa7f1231a1aa56d695e626043252b7be6a90c4ce" + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php74/zipball/aa7f1231a1aa56d695e626043252b7be6a90c4ce", - "reference": "aa7f1231a1aa56d695e626043252b7be6a90c4ce", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", "shasum": "" }, "require": { @@ -4084,7 +3985,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4096,8 +3997,11 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php74\\": "" - } + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4117,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", @@ -4126,7 +4030,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php74/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" }, "funding": [ { @@ -4142,20 +4046,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { - "name": "symfony/polyfill-php80", - "version": "v1.27.0", + "name": "symfony/polyfill-php81", + "version": "v1.28.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936" + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "7581cd600fa9fd681b797d00b02f068e2f13263b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", - "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/7581cd600fa9fd681b797d00b02f068e2f13263b", + "reference": "7581cd600fa9fd681b797d00b02f068e2f13263b", "shasum": "" }, "require": { @@ -4164,7 +4068,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.27-dev" + "dev-main": "1.28-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4176,7 +4080,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" + "Symfony\\Polyfill\\Php81\\": "" }, "classmap": [ "Resources/stubs" @@ -4187,10 +4091,6 @@ "MIT" ], "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -4200,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", @@ -4209,7 +4109,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.28.0" }, "funding": [ { @@ -4225,20 +4125,20 @@ "type": "tidelift" } ], - "time": "2022-11-03T14:55:06+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": { @@ -4271,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": [ { @@ -4287,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", @@ -4374,16 +4274,16 @@ }, { "name": "symfony/string", - "version": "v5.4.15", + "version": "v5.4.26", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "571334ce9f687e3e6af72db4d3b2a9431e4fd9ed" + "reference": "1181fe9270e373537475e826873b5867b863883c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/571334ce9f687e3e6af72db4d3b2a9431e4fd9ed", - "reference": "571334ce9f687e3e6af72db4d3b2a9431e4fd9ed", + "url": "https://api.github.com/repos/symfony/string/zipball/1181fe9270e373537475e826873b5867b863883c", + "reference": "1181fe9270e373537475e826873b5867b863883c", "shasum": "" }, "require": { @@ -4440,7 +4340,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.15" + "source": "https://github.com/symfony/string/tree/v5.4.26" }, "funding": [ { @@ -4456,22 +4356,22 @@ "type": "tidelift" } ], - "time": "2022-10-05T15:16:54+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": { @@ -4481,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", @@ -4539,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": [ { @@ -4551,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", @@ -4623,62 +4571,6 @@ ], "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", @@ -4739,74 +4631,12 @@ "time": "2021-10-08T21:21:46+00:00" }, { - "name": "loophp/phposinfo", - "version": "1.7.2", + "name": "myclabs/deep-copy", + "version": "1.11.0", "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", - "source": { - "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" }, "dist": { "type": "zip", @@ -4859,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", @@ -5029,21 +4908,21 @@ }, { "name": "phpstan/phpstan-deprecation-rules", - "version": "1.1.1", + "version": "1.1.4", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", - "reference": "2c6792eda026d9c474c14aa018aed312686714db" + "reference": "089d8a8258ed0aeefdc7b68b6c3d25572ebfdbaa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/2c6792eda026d9c474c14aa018aed312686714db", - "reference": "2c6792eda026d9c474c14aa018aed312686714db", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/089d8a8258ed0aeefdc7b68b6c3d25572ebfdbaa", + "reference": "089d8a8258ed0aeefdc7b68b6c3d25572ebfdbaa", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.9.3" + "phpstan/phpstan": "^1.10.3" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", @@ -5071,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.1.1" + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/1.1.4" }, - "time": "2022-12-13T14:26:20+00:00" + "time": "2023-08-05T09:02:04+00:00" }, { "name": "phpstan/phpstan-nette", - "version": "1.2.0", + "version": "1.2.9", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-nette.git", - "reference": "1e32a0ff252d37ee57d91fdc2fdf21256b28df07" + "reference": "0e3a6805917811d685e59bb83c2286315f2f6d78" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-nette/zipball/1e32a0ff252d37ee57d91fdc2fdf21256b28df07", - "reference": "1e32a0ff252d37ee57d91fdc2fdf21256b28df07", + "url": "https://api.github.com/repos/phpstan/phpstan-nette/zipball/0e3a6805917811d685e59bb83c2286315f2f6d78", + "reference": "0e3a6805917811d685e59bb83c2286315f2f6d78", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.9.4" + "phpstan/phpstan": "^1.10" }, "conflict": { "nette/application": "<2.3.0", @@ -5102,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", @@ -5132,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.2.0" - }, - "time": "2022-12-16T11:14:15+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/" - } + "source": "https://github.com/phpstan/phpstan-nette/tree/1.2.9" }, - "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.3.3", + "version": "1.3.15", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "54a24bd23e9e80ee918cdc24f909d376c2e273f7" + "reference": "70ecacc64fe8090d8d2a33db5a51fe8e88acd93a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/54a24bd23e9e80ee918cdc24f909d376c2e273f7", - "reference": "54a24bd23e9e80ee918cdc24f909d376c2e273f7", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/70ecacc64fe8090d8d2a33db5a51fe8e88acd93a", + "reference": "70ecacc64fe8090d8d2a33db5a51fe8e88acd93a", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.9.3" + "phpstan/phpstan": "^1.10" }, "conflict": { "phpunit/phpunit": "<7.0" @@ -5211,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", @@ -5235,35 +5064,35 @@ "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.3.3" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.3.15" }, - "time": "2022-12-21T15:25:00+00:00" + "time": "2023-10-09T18:58:39+00:00" }, { "name": "phpstan/phpstan-strict-rules", - "version": "1.5.x-dev", + "version": "1.5.2", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "c7531266bd8e854d520a295c96a5d16630cb82a6" + "reference": "7a50e9662ee9f3942e4aaaf3d603653f60282542" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/c7531266bd8e854d520a295c96a5d16630cb82a6", - "reference": "c7531266bd8e854d520a295c96a5d16630cb82a6", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/7a50e9662ee9f3942e4aaaf3d603653f60282542", + "reference": "7a50e9662ee9f3942e4aaaf3d603653f60282542", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.10" + "phpstan/phpstan": "^1.10.34" }, "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": { @@ -5284,29 +5113,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.5.x" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.5.2" }, - "time": "2023-01-13T13:18:41+00:00" + "time": "2023-10-30T14:35:06+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", @@ -5321,8 +5150,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": { @@ -5355,7 +5184,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": [ { @@ -5363,7 +5193,7 @@ "type": "github" } ], - "time": "2022-03-07T09:28:20+00:00" + "time": "2023-12-22T06:47:57+00:00" }, { "name": "phpunit/php-file-iterator", @@ -5704,62 +5534,6 @@ ], "time": "2022-08-22T14:01:36+00:00" }, - { - "name": "rector/rector", - "version": "0.15.2", - "source": { - "type": "git", - "url": "https://github.com/rectorphp/rector.git", - "reference": "5bc89fa73d0be2769e02e49a0e924c95b1842093" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/5bc89fa73d0be2769e02e49a0e924c95b1842093", - "reference": "5bc89fa73d0be2769e02e49a0e924c95b1842093", - "shasum": "" - }, - "require": { - "php": "^7.2|^8.0", - "phpstan/phpstan": "^1.9.4" - }, - "conflict": { - "rector/rector-doctrine": "*", - "rector/rector-downgrade-php": "*", - "rector/rector-php-parser": "*", - "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.15.2" - }, - "funding": [ - { - "url": "https://github.com/tomasvotruba", - "type": "github" - } - ], - "time": "2022-12-24T12:55:36+00:00" - }, { "name": "sebastian/cli-parser", "version": "1.0.1", @@ -6003,20 +5777,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": { @@ -6048,7 +5822,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": [ { @@ -6056,7 +5830,7 @@ "type": "github" } ], - "time": "2020-10-26T15:52:27+00:00" + "time": "2023-12-22T06:19:30+00:00" }, { "name": "sebastian/diff", @@ -6126,16 +5900,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": { @@ -6177,7 +5951,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": [ { @@ -6185,7 +5959,7 @@ "type": "github" } ], - "time": "2022-04-03T09:37:03+00:00" + "time": "2023-02-03T06:03:51+00:00" }, { "name": "sebastian/exporter", @@ -6330,20 +6104,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": { @@ -6375,7 +6149,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": [ { @@ -6383,7 +6157,7 @@ "type": "github" } ], - "time": "2020-11-28T06:42:11+00:00" + "time": "2023-12-22T06:20:34+00:00" }, { "name": "sebastian/object-enumerator", @@ -6725,67 +6499,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" + ], + "description": "Simple tool to find ambiguous classes or any other name duplicates within your project.", + "keywords": [ + "ambiguous", + "autoload", + "autoloading", + "classname", + "collision", + "namespace" ], - "time": "2020-11-11T09:19:24+00:00" + "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", @@ -6836,166 +6669,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": [], @@ -7013,5 +6686,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 0dfe15c4bc..b84564b6fb 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -7,6 +7,7 @@ parameters: explicitMixedViaIsArray: true arrayFilter: true arrayUnpacking: true + arrayValues: true nodeConnectingVisitorCompatibility: false nodeConnectingVisitorRule: true disableCheckMissingIterableValueType: true @@ -16,14 +17,19 @@ 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 invarianceComposition: true alwaysTrueAlwaysReported: true disableUnreachableBranchesRules: true @@ -31,3 +37,17 @@ parameters: 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 diff --git a/conf/config.level0.neon b/conf/config.level0.neon index 370de09fa6..58aa28e2af 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -22,6 +22,8 @@ conditionalTags: 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 @@ -50,41 +52,60 @@ 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\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 @@ -143,6 +164,9 @@ services: class: PHPStan\Rules\Methods\OverridingMethodRule arguments: checkPhpDocMethodSignatures: %checkPhpDocMethodSignatures% + genericPrototypeMessage: %featureToggles.genericPrototypeMessage% + finalByPhpDoc: %featureToggles.finalByPhpDoc% + checkMissingOverrideMethodAttribute: %checkMissingOverrideMethodAttribute% tags: - phpstan.rules.rule @@ -258,13 +282,6 @@ services: tags: - phpstan.rules.rule - - - class: PHPStan\Rules\Classes\LocalTypeAliasesRule - arguments: - globalTypeAliases: %typeAliases% - tags: - - phpstan.rules.rule - - class: PHPStan\Reflection\ConstructorsHelper arguments: @@ -272,3 +289,6 @@ services: - class: PHPStan\Rules\Methods\MissingMagicSerializationMethodsRule + + - + class: PHPStan\Rules\Constants\MagicConstantContextRule diff --git a/conf/config.level2.neon b/conf/config.level2.neon index 377d1a605d..b46b417f5d 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 @@ -39,10 +41,13 @@ rules: - 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\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: @@ -55,6 +60,8 @@ conditionalTags: phpstan.rules.rule: %featureToggles.illegalConstructorMethodCall% PHPStan\Rules\PhpDoc\VarTagChangedExpressionTypeRule: phpstan.rules.rule: %featureToggles.varTagType% + PHPStan\Rules\Generics\PropertyVarianceRule: + phpstan.rules.rule: %featureToggles.propertyVariance% services: - @@ -63,6 +70,19 @@ 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 - @@ -77,6 +97,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: @@ -84,6 +111,12 @@ 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 - @@ -92,3 +125,7 @@ services: checkTypeAgainstNativeType: %featureToggles.varTagType% tags: - phpstan.rules.rule + - + class: PHPStan\Rules\Generics\PropertyVarianceRule + arguments: + readOnlyByPhpDoc: %featureToggles.readOnlyByPhpDoc% 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 c2199a29dc..f9bb627f91 100644 --- a/conf/config.level4.neon +++ b/conf/config.level4.neon @@ -3,11 +3,9 @@ includes: rules: - PHPStan\Rules\Arrays\DeadForeachRule - - 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 @@ -27,6 +25,12 @@ 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\TooWideTypehints\TooWideFunctionParameterOutTypeRule: + phpstan.rules.rule: %featureToggles.paramOutType% + PHPStan\Rules\TooWideTypehints\TooWideMethodParameterOutTypeRule: + phpstan.rules.rule: %featureToggles.paramOutType% parameters: checkAdvancedIsset: true @@ -37,6 +41,7 @@ services: arguments: checkAlwaysTrueInstanceof: %checkAlwaysTrueInstanceof% treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% tags: - phpstan.rules.rule @@ -45,6 +50,7 @@ services: arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% bleedingEdge: %featureToggles.bleedingEdge% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% tags: - phpstan.rules.rule @@ -53,6 +59,7 @@ services: arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% bleedingEdge: %featureToggles.bleedingEdge% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% tags: - phpstan.rules.rule @@ -60,6 +67,14 @@ services: class: PHPStan\Rules\Comparison\BooleanNotConstantConditionRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% + tags: + - phpstan.rules.rule + + - + class: PHPStan\Rules\DeadCode\NoopRule + arguments: + logicalXor: %featureToggles.logicalXor% tags: - phpstan.rules.rule @@ -83,6 +98,7 @@ services: class: PHPStan\Rules\Comparison\ElseIfConstantConditionRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% tags: - phpstan.rules.rule @@ -98,6 +114,7 @@ services: arguments: checkAlwaysTrueCheckTypeFunctionCall: %checkAlwaysTrueCheckTypeFunctionCall% treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% tags: - phpstan.rules.rule @@ -106,6 +123,7 @@ services: arguments: checkAlwaysTrueCheckTypeFunctionCall: %checkAlwaysTrueCheckTypeFunctionCall% treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% tags: - phpstan.rules.rule @@ -114,15 +132,23 @@ 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\Comparison\MatchExpressionRule arguments: checkAlwaysTrueStrictComparison: %checkAlwaysTrueStrictComparison% disableUnreachable: %featureToggles.disableUnreachableBranchesRules% reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% tags: - phpstan.rules.rule @@ -146,6 +172,8 @@ services: class: PHPStan\Rules\Comparison\ConstantLooseComparisonRule arguments: checkAlwaysTrueLooseComparison: %checkAlwaysTrueLooseComparison% + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + reportAlwaysTrueInLastCondition: %reportAlwaysTrueInLastCondition% - class: PHPStan\Rules\Comparison\TernaryOperatorConstantConditionRule @@ -188,6 +216,7 @@ services: class: PHPStan\Rules\TooWideTypehints\TooWideMethodReturnTypehintRule arguments: checkProtectedAndPublicMethods: %checkTooWideReturnTypesInProtectedAndPublicMethods% + alwaysCheckFinal: %featureToggles.alwaysCheckTooWideReturnTypeFinalMethods% tags: - phpstan.rules.rule @@ -204,3 +233,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 7941ae444b..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 @@ -25,3 +29,11 @@ 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.neon b/conf/config.neon index 512320ccd4..a1852419bf 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,14 +52,19 @@ 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 invarianceComposition: false alwaysTrueAlwaysReported: false disableUnreachableBranchesRules: false @@ -61,6 +72,20 @@ parameters: 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 fileExtensions: - php checkAdvancedIsset: false @@ -100,6 +125,8 @@ parameters: reportMaybesInPropertyPhpDocTypes: false reportStaticMethodSignatures: false reportWrongPhpDocTypeInVarTag: false + reportAnyTypeWideningInVarTag: false + checkMissingOverrideMethodAttribute: false mixinExcludeClasses: [] scanFiles: [] scanDirectories: [] @@ -114,6 +141,7 @@ parameters: polluteScopeWithAlwaysIterableForeach: true propertyAlwaysWrittenTags: [] propertyAlwaysReadTags: [] + fixerTmpDir: %pro.tmpDir% #unused additionalConstructors: [] treatPhpDocTypesAsCertain: true usePathConstantsAsConstantString: false @@ -146,6 +174,7 @@ parameters: - ../stubs/ImagickPixel.stub - ../stubs/PDOStatement.stub - ../stubs/date.stub + - ../stubs/ibm_db2.stub - ../stubs/mysqli.stub - ../stubs/zip.stub - ../stubs/dom.stub @@ -218,6 +247,11 @@ parameters: editorUrl: null editorUrlTitle: null errorFormat: null + sysGetTempDir: ::sys_get_temp_dir() + pro: + dnsServers: + - '1.1.1.2' + tmpDir: %sysGetTempDir%/phpstan-fixer __validate: true extensions: @@ -226,208 +260,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() - listType: bool() - missingMagicSerializationRule: bool() - nullContextForVoidReturningFunctions: bool() - unescapeStrings: bool() - duplicateStubs: bool() - invarianceComposition: bool() - alwaysTrueAlwaysReported: bool() - disableUnreachableBranchesRules: bool() - varTagType: bool() - closureDefaultParameterTypeRule: bool() - newRuleLevelHelper: bool() - instanceofType: 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() - 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() - usePathConstantsAsConstantString: 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()) - editorUrlTitle: 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 @@ -492,6 +324,11 @@ services: tags: - phpstan.parser.richParserNodeVisitor + - + class: PHPStan\Parser\MagicConstantParamDefaultVisitor + tags: + - phpstan.parser.richParserNodeVisitor + - class: PHPStan\Parser\NewAssignedToPropertyVisitor tags: @@ -545,6 +382,8 @@ services: - class: PHPStan\PhpDocParser\Parser\TypeParser + arguments: + quoteAwareConstExprString: %featureToggles.unescapeStrings% - class: PHPStan\PhpDocParser\Parser\ConstExprParser @@ -554,6 +393,9 @@ services: class: PHPStan\PhpDocParser\Parser\PhpDocParser arguments: requireWhitespaceBeforeDescription: %featureToggles.phpDocParserRequireWhitespaceBeforeDescription% + preserveTypeAliasesWithInvalidTypes: true + usedAttributes: + lines: %featureToggles.phpDocParserIncludeLines% - class: PHPStan\PhpDoc\ConstExprParserFactory @@ -594,6 +436,11 @@ services: tags: - phpstan.stubFilesExtension + - + class: PHPStan\PhpDoc\SocketSelectStubFilesExtension + tags: + - phpstan.stubFilesExtension + - class: PHPStan\PhpDoc\DefaultStubFilesProvider arguments: @@ -602,6 +449,16 @@ services: autowired: - PHPStan\PhpDoc\StubFilesProvider + - + class: PHPStan\PhpDoc\JsonValidateStubFilesExtension + tags: + - phpstan.stubFilesExtension + + - + class: PHPStan\PhpDoc\ReflectionEnumStubFilesExtension + tags: + - phpstan.stubFilesExtension + - class: PHPStan\Analyser\Analyser arguments: @@ -643,6 +500,9 @@ services: earlyTerminatingFunctionCalls: %earlyTerminatingFunctionCalls% implicitThrows: %exceptions.implicitThrows% treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + detectDeadTypeInMultiCatch: %featureToggles.detectDeadTypeInMultiCatch% + universalObjectCratesClasses: %universalObjectCratesClasses% + paramOutType: %featureToggles.paramOutType% - class: PHPStan\Analyser\ConstantResolver @@ -656,7 +516,6 @@ services: arguments: scanFileFinder: @fileFinderScan cacheFilePath: %resultCachePath% - tempResultCachePath: %tempResultCachePath% analysedPaths: %analysedPaths% composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% usedLevel: %usedLevel% @@ -670,7 +529,6 @@ services: class: PHPStan\Analyser\ResultCache\ResultCacheClearer arguments: cacheFilePath: %resultCachePath% - tempResultCachePath: %tempResultCachePath% - class: PHPStan\Cache\Cache @@ -697,8 +555,8 @@ services: arguments: analysedPaths: %analysedPaths% currentWorkingDirectory: %currentWorkingDirectory% - fixerTmpDir: %fixerTmpDir% - maximumNumberOfProcesses: %parallel.maximumNumberOfProcesses% + proTmpDir: %pro.tmpDir% + dnsServers: %pro.dnsServers% - class: PHPStan\Dependency\DependencyResolver @@ -737,8 +595,6 @@ services: usedLevel: %usedLevel% generateBaselineFile: %generateBaselineFile% cliAutoloadFile: %cliAutoloadFile% - singleReflectionFile: %singleReflectionFile% - singleReflectionInsteadOfFile: %singleReflectionInsteadOfFile% - class: PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider @@ -748,6 +604,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 @@ -804,6 +664,11 @@ services: tags: - phpstan.parser.richParserNodeVisitor + - + class: PHPStan\Parser\DeclarePositionVisitor + tags: + - phpstan.parser.richParserNodeVisitor + - class: PHPStan\Parallel\ParallelAnalyser arguments: @@ -868,6 +733,12 @@ services: - class: PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocatorRepository + - + class: PHPStan\Reflection\RequireExtension\RequireExtendsMethodsClassReflectionExtension + + - + class: PHPStan\Reflection\RequireExtension\RequireExtendsPropertiesClassReflectionExtension + - class: PHPStan\Reflection\Mixin\MixinMethodsClassReflectionExtension tags: @@ -887,7 +758,6 @@ services: arguments: parser: @defaultAnalysisParser inferPrivatePropertyTypeFromConstructor: %inferPrivatePropertyTypeFromConstructor% - universalObjectCratesClasses: %universalObjectCratesClasses% - implement: PHPStan\Reflection\Php\PhpMethodReflectionFactory @@ -911,6 +781,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 @@ -925,6 +811,8 @@ services: - class: PHPStan\Reflection\SignatureMap\FunctionSignatureMapProvider + arguments: + stricterFunctionMap: %featureToggles.stricterFunctionMap% autowired: - PHPStan\Reflection\SignatureMap\FunctionSignatureMapProvider @@ -954,11 +842,22 @@ services: reportMaybes: %reportMaybes% bleedingEdge: %featureToggles.bleedingEdge% + - + 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: @@ -1039,6 +938,9 @@ services: - class: PHPStan\Rules\Generics\VarianceCheck + arguments: + checkParamOutVariance: %featureToggles.paramOutVariance% + strictStaticVariance: %featureToggles.strictStaticMethodTemplateTypeVariance% - class: PHPStan\Rules\IssetCheck @@ -1065,9 +967,12 @@ services: arguments: reportMaybes: %reportMaybesInMethodSignatures% reportStatic: %reportStaticMethodSignatures% + abstractTraitMethod: %featureToggles.abstractTraitMethod% - class: PHPStan\Rules\Methods\MethodParameterComparisonHelper + arguments: + genericPrototypeMessage: %featureToggles.genericPrototypeMessage% - class: PHPStan\Rules\MissingTypehintCheck @@ -1084,6 +989,9 @@ services: - class: PHPStan\Rules\Constants\LazyAlwaysUsedClassConstantsExtensionProvider + - + class: PHPStan\Rules\Methods\LazyAlwaysUsedMethodExtensionProvider + - class: PHPStan\Rules\PhpDoc\ConditionalReturnTypeRuleHelper @@ -1093,10 +1001,17 @@ services: - 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 @@ -1121,6 +1036,9 @@ services: - class: PHPStan\Rules\UnusedFunctionParametersCheck + - + class: PHPStan\Rules\TooWideTypehints\TooWideParameterOutTypeCheck + - class: PHPStan\Type\FileTypeMapper arguments: @@ -1289,6 +1207,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: @@ -1321,6 +1249,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: @@ -1341,6 +1277,9 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\DateFunctionReturnTypeHelper + - class: PHPStan\Type\Php\DateFormatFunctionReturnTypeExtension tags: @@ -1405,6 +1344,11 @@ services: tags: - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Type\Php\DsMapDynamicMethodThrowTypeExtension + tags: + - phpstan.dynamicMethodThrowTypeExtension + - class: PHPStan\Type\Php\DioStatDynamicFunctionReturnTypeExtension tags: @@ -1418,11 +1362,21 @@ services: - 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: @@ -1438,6 +1392,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\GettypeFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\GettimeofdayDynamicFunctionReturnTypeExtension tags: @@ -1452,6 +1411,11 @@ services: tags: - phpstan.dynamicFunctionThrowTypeExtension + - + class: PHPStan\Type\Php\IniGetReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\JsonThrowTypeExtension tags: @@ -1593,6 +1557,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\SetTypeFunctionTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - class: PHPStan\Type\Php\StrCaseFunctionsReturnTypeExtension tags: @@ -1603,6 +1572,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\StrIncrementDecrementFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\StrPadFunctionReturnTypeExtension tags: @@ -1898,6 +1872,7 @@ services: arguments: parser: @currentPhpVersionPhpParser lexer: @currentPhpVersionLexer + enableIgnoreErrorsWithinPhpDocs: %featureToggles.enableIgnoreErrorsWithinPhpDocs% autowired: no currentPhpVersionSimpleParser: @@ -2003,6 +1978,7 @@ services: class: PHPStan\Reflection\BetterReflection\BetterReflectionProvider arguments: reflector: @betterReflectionReflector + universalObjectCratesClasses: %universalObjectCratesClasses% autowired: false - @@ -2015,10 +1991,11 @@ services: analysedPaths: %analysedPaths% composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% analysedPathsFromConfig: %analysedPathsFromConfig% - singleReflectionFile: %singleReflectionFile% - implement: PHPStan\Reflection\BetterReflection\BetterReflectionProviderFactory + arguments: + universalObjectCratesClasses: %universalObjectCratesClasses% - class: PHPStan\Reflection\BetterReflection\SourceStubber\PhpStormStubsSourceStubberFactory @@ -2058,7 +2035,6 @@ services: currentPhpVersionRichParser: @currentPhpVersionRichParser currentPhpVersionSimpleParser: @currentPhpVersionSimpleParser php8Parser: @php8Parser - singleReflectionFile: %singleReflectionFile% autowired: false # Error formatters 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..0718ec9445 --- /dev/null +++ b/conf/parametersSchema.neon @@ -0,0 +1,227 @@ +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() + 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() + ]) + 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() + 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()) + ?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() #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-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 @@ +=7.0.0", - "psr/http-message": "^1.0" + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { @@ -1826,7 +1835,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", @@ -1841,31 +1850,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": "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": { @@ -1894,9 +1903,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": "ralouphie/getallheaders", @@ -1944,16 +1953,16 @@ }, { "name": "symfony/console", - "version": "v6.2.3", + "version": "v6.2.10", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "0f579613e771dba2dbb8211c382342a641f5da06" + "reference": "12288d9f4500f84a4d02254d4aa968b15488476f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/0f579613e771dba2dbb8211c382342a641f5da06", - "reference": "0f579613e771dba2dbb8211c382342a641f5da06", + "url": "https://api.github.com/repos/symfony/console/zipball/12288d9f4500f84a4d02254d4aa968b15488476f", + "reference": "12288d9f4500f84a4d02254d4aa968b15488476f", "shasum": "" }, "require": { @@ -2015,12 +2024,12 @@ "homepage": "https://symfony.com/", "keywords": [ "cli", - "command line", + "command-line", "console", "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.2.3" + "source": "https://github.com/symfony/console/tree/v6.2.10" }, "funding": [ { @@ -2036,20 +2045,20 @@ "type": "tidelift" } ], - "time": "2022-12-28T14:26:22+00:00" + "time": "2023-04-28T13:37:43+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.2.0", + "version": "v3.2.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "1ee04c65529dea5d8744774d474e7cbd2f1206d3" + "reference": "e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/1ee04c65529dea5d8744774d474e7cbd2f1206d3", - "reference": "1ee04c65529dea5d8744774d474e7cbd2f1206d3", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e", + "reference": "e2d1534420bd723d0ef5aec58a22c5fe60ce6f5e", "shasum": "" }, "require": { @@ -2087,7 +2096,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.2.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.2.1" }, "funding": [ { @@ -2103,20 +2112,20 @@ "type": "tidelift" } ], - "time": "2022-11-25T10:21:52+00:00" + "time": "2023-03-01T10:25:55+00:00" }, { "name": "symfony/finder", - "version": "v6.2.3", + "version": "v6.2.7", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "81eefbddfde282ee33b437ba5e13d7753211ae8e" + "reference": "20808dc6631aecafbe67c186af5dcb370be3a0eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/81eefbddfde282ee33b437ba5e13d7753211ae8e", - "reference": "81eefbddfde282ee33b437ba5e13d7753211ae8e", + "url": "https://api.github.com/repos/symfony/finder/zipball/20808dc6631aecafbe67c186af5dcb370be3a0eb", + "reference": "20808dc6631aecafbe67c186af5dcb370be3a0eb", "shasum": "" }, "require": { @@ -2151,7 +2160,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com/", "support": { - "source": "https://github.com/symfony/finder/tree/v6.2.3" + "source": "https://github.com/symfony/finder/tree/v6.2.7" }, "funding": [ { @@ -2167,20 +2176,20 @@ "type": "tidelift" } ], - "time": "2022-12-22T17:55:15+00:00" + "time": "2023-02-16T09:57:23+00:00" }, { "name": "symfony/options-resolver", - "version": "v6.1.0", + "version": "v6.2.7", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "a3016f5442e28386ded73c43a32a5b68586dd1c4" + "reference": "aa0e85b53bbb2b4951960efd61d295907eacd629" }, "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/aa0e85b53bbb2b4951960efd61d295907eacd629", + "reference": "aa0e85b53bbb2b4951960efd61d295907eacd629", "shasum": "" }, "require": { @@ -2218,7 +2227,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v6.1.0" + "source": "https://github.com/symfony/options-resolver/tree/v6.2.7" }, "funding": [ { @@ -2234,7 +2243,7 @@ "type": "tidelift" } ], - "time": "2022-02-25T11:15:52+00:00" + "time": "2023-02-14T08:44:56+00:00" }, { "name": "symfony/polyfill-ctype", @@ -2651,16 +2660,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.2.0", + "version": "v3.2.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "aac98028c69df04ee77eb69b96b86ee51fbf4b75" + "reference": "a8c9cedf55f314f3a186041d19537303766df09a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/aac98028c69df04ee77eb69b96b86ee51fbf4b75", - "reference": "aac98028c69df04ee77eb69b96b86ee51fbf4b75", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/a8c9cedf55f314f3a186041d19537303766df09a", + "reference": "a8c9cedf55f314f3a186041d19537303766df09a", "shasum": "" }, "require": { @@ -2716,7 +2725,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.2.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.2.1" }, "funding": [ { @@ -2732,20 +2741,20 @@ "type": "tidelift" } ], - "time": "2022-11-25T10:21:52+00:00" + "time": "2023-03-01T10:32:47+00:00" }, { "name": "symfony/string", - "version": "v6.2.2", + "version": "v6.2.8", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "863219fd713fa41cbcd285a79723f94672faff4d" + "reference": "193e83bbd6617d6b2151c37fff10fa7168ebddef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/863219fd713fa41cbcd285a79723f94672faff4d", - "reference": "863219fd713fa41cbcd285a79723f94672faff4d", + "url": "https://api.github.com/repos/symfony/string/zipball/193e83bbd6617d6b2151c37fff10fa7168ebddef", + "reference": "193e83bbd6617d6b2151c37fff10fa7168ebddef", "shasum": "" }, "require": { @@ -2802,7 +2811,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.2.2" + "source": "https://github.com/symfony/string/tree/v6.2.8" }, "funding": [ { @@ -2818,7 +2827,7 @@ "type": "tidelift" } ], - "time": "2022-12-14T16:11:27+00:00" + "time": "2023-03-20T16:06:02+00:00" } ], "packages-dev": [ @@ -2894,16 +2903,16 @@ }, { "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": { @@ -2941,7 +2950,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": [ { @@ -2949,20 +2958,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.2", + "version": "v4.15.4", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc" + "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc", - "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6bb5176bc4af8bcb7d926f88718db9b96a2d4290", + "reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290", "shasum": "" }, "require": { @@ -3003,9 +3012,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.4" }, - "time": "2022-11-12T15:38:23+00:00" + "time": "2023-03-05T19:49:14+00:00" }, { "name": "phar-io/manifest", @@ -3120,23 +3129,23 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.23", + "version": "9.2.26", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c" + "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c", - "reference": "9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", + "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1", "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", @@ -3151,8 +3160,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": { @@ -3185,7 +3194,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.23" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.26" }, "funding": [ { @@ -3193,7 +3202,7 @@ "type": "github" } ], - "time": "2022-12-28T12:41:10+00:00" + "time": "2023-03-06T12:58:08+00:00" }, { "name": "phpunit/php-file-iterator", @@ -3438,16 +3447,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.28", + "version": "9.6.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "954ca3113a03bf780d22f07bf055d883ee04b65e" + "reference": "c993f0d3b0489ffc42ee2fe0bd645af1538a63b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/954ca3113a03bf780d22f07bf055d883ee04b65e", - "reference": "954ca3113a03bf780d22f07bf055d883ee04b65e", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c993f0d3b0489ffc42ee2fe0bd645af1538a63b2", + "reference": "c993f0d3b0489ffc42ee2fe0bd645af1538a63b2", "shasum": "" }, "require": { @@ -3480,8 +3489,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" @@ -3489,7 +3498,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.5-dev" + "dev-master": "9.6-dev" } }, "autoload": { @@ -3520,7 +3529,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.28" + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.7" }, "funding": [ { @@ -3536,7 +3546,7 @@ "type": "tidelift" } ], - "time": "2023-01-14T12:32:24+00:00" + "time": "2023-04-14T08:58:40+00:00" }, { "name": "sebastian/cli-parser", @@ -3838,16 +3848,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": { @@ -3892,7 +3902,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": [ { @@ -3900,20 +3910,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": { @@ -3955,7 +3965,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": [ { @@ -3963,7 +3973,7 @@ "type": "github" } ], - "time": "2022-04-03T09:37:03+00:00" + "time": "2023-02-03T06:03:51+00:00" }, { "name": "sebastian/exporter", @@ -4277,16 +4287,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": { @@ -4325,10 +4335,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": [ { @@ -4336,7 +4346,7 @@ "type": "github" } ], - "time": "2020-10-26T13:17:30+00:00" + "time": "2023-02-03T06:07:39+00:00" }, { "name": "sebastian/resource-operations", @@ -4395,16 +4405,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": { @@ -4439,7 +4449,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": [ { @@ -4447,7 +4457,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/issue-bot/playground.neon b/issue-bot/playground.neon new file mode 100644 index 0000000000..fe80928a5f --- /dev/null +++ b/issue-bot/playground.neon @@ -0,0 +1,4 @@ +rules: + - PHPStan\Rules\Playground\FunctionNeverRule + - PHPStan\Rules\Playground\MethodNeverRule + - PHPStan\Rules\Playground\NotAnalysedTraitRule diff --git a/issue-bot/src/Console/DownloadCommand.php b/issue-bot/src/Console/DownloadCommand.php index ee6302c6f0..511ec78d07 100644 --- a/issue-bot/src/Console/DownloadCommand.php +++ b/issue-bot/src/Console/DownloadCommand.php @@ -99,12 +99,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $matrix = []; - foreach ([70100, 70200, 70300, 70400, 80000, 80100, 80200] as $phpVersion) { + 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 = [70100, 70200, 70300, 70400, 80000]; + $resultPhpVersions = [70200, 70300, 70400, 80000]; } if (!in_array(80100, $resultPhpVersions, true)) { @@ -113,6 +113,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (!in_array(80200, $resultPhpVersions, true)) { $resultPhpVersions[] = 80200; } + if (!in_array(80300, $resultPhpVersions, true)) { + $resultPhpVersions[] = 80300; + } if (!in_array($phpVersion, $resultPhpVersions, true)) { continue; @@ -124,11 +127,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int 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++; } } diff --git a/issue-bot/src/Console/EvaluateCommand.php b/issue-bot/src/Console/EvaluateCommand.php index 09c3d8c3ba..c9896f4086 100644 --- a/issue-bot/src/Console/EvaluateCommand.php +++ b/issue-bot/src/Console/EvaluateCommand.php @@ -96,7 +96,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int throw new Exception(sprintf('Hash %s does not exist in new results.', $hash)); } - $newTabs = $this->tabCreator->create($this->filterErrors($originalErrors, $newResults[$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; diff --git a/issue-bot/src/Console/RunCommand.php b/issue-bot/src/Console/RunCommand.php index 94f9ce7ffa..763db57cd0 100644 --- a/issue-bot/src/Console/RunCommand.php +++ b/issue-bot/src/Console/RunCommand.php @@ -71,7 +71,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int */ private function analyseHash(OutputInterface $output, int $phpVersion, PlaygroundResult $result): array { - $configFiles = []; + $configFiles = [ + __DIR__ . '/../../playground.neon', + ]; if ($result->isBleedingEdge()) { $configFiles[] = __DIR__ . '/../../../conf/bleedingEdge.neon'; } @@ -124,7 +126,7 @@ private function analyseHash(OutputInterface $output, int $phpVersion, Playgroun if (strpos($messageText, 'Internal error') !== false) { throw new Exception(sprintf('While analysing %s: %s', $hash, $messageText)); } - $errors[] = new PlaygroundError($message['line'] ?? -1, $messageText); + $errors[] = new PlaygroundError($message['line'] ?? -1, $messageText, $message['identifier'] ?? null); } } diff --git a/issue-bot/src/Playground/PlaygroundClient.php b/issue-bot/src/Playground/PlaygroundClient.php index 6bd2fdd5c6..43cd6ea9d3 100644 --- a/issue-bot/src/Playground/PlaygroundClient.php +++ b/issue-bot/src/Playground/PlaygroundClient.php @@ -24,7 +24,7 @@ public function getResult(string $hash): PlaygroundResult $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']), array_values($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( diff --git a/issue-bot/src/Playground/PlaygroundError.php b/issue-bot/src/Playground/PlaygroundError.php index ae7ef036a2..1e55ac88b3 100644 --- a/issue-bot/src/Playground/PlaygroundError.php +++ b/issue-bot/src/Playground/PlaygroundError.php @@ -5,7 +5,7 @@ class PlaygroundError { - public function __construct(private int $line, private string $message) + public function __construct(private int $line, private string $message, private ?string $identifier) { } @@ -19,4 +19,9 @@ public function getMessage(): string return $this->message; } + public function getIdentifier(): ?string + { + return $this->identifier; + } + } diff --git a/issue-bot/src/Playground/TabCreator.php b/issue-bot/src/Playground/TabCreator.php index 7c336f270f..a74da457a4 100644 --- a/issue-bot/src/Playground/TabCreator.php +++ b/issue-bot/src/Playground/TabCreator.php @@ -2,9 +2,12 @@ namespace PHPStan\IssueBot\Playground; +use function array_map; use function count; use function floor; use function ksort; +use function sprintf; +use function str_starts_with; use function usort; use const SORT_NUMERIC; @@ -23,6 +26,21 @@ public function create(array $versionedErrors): array $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, diff --git a/issue-bot/tests/Playground/TabCreatorTest.php b/issue-bot/tests/Playground/TabCreatorTest.php index befa992129..47bf147308 100644 --- a/issue-bot/tests/Playground/TabCreatorTest.php +++ b/issue-bot/tests/Playground/TabCreatorTest.php @@ -30,53 +30,77 @@ public function dataCreate(): array [ [ 70100 => [ - new PlaygroundError(2, 'Foo'), + new PlaygroundError(2, 'Foo', null), ], 70200 => [ - new PlaygroundError(2, 'Foo'), + new PlaygroundError(2, 'Foo', null), ], ], [ new PlaygroundResultTab('PHP 7.1 – 7.2 (1 error)', [ - new PlaygroundError(2, 'Foo'), + new PlaygroundError(2, 'Foo', null), ]), ], ], [ [ 70100 => [ - new PlaygroundError(2, 'Foo'), - new PlaygroundError(3, 'Foo'), + new PlaygroundError(2, 'Foo', null), + new PlaygroundError(3, 'Foo', null), ], 70200 => [ - new PlaygroundError(2, 'Foo'), - new PlaygroundError(3, 'Foo'), + new PlaygroundError(2, 'Foo', null), + new PlaygroundError(3, 'Foo', null), ], ], [ new PlaygroundResultTab('PHP 7.1 – 7.2 (2 errors)', [ - new PlaygroundError(2, 'Foo'), - new PlaygroundError(3, 'Foo'), + new PlaygroundError(2, 'Foo', null), + new PlaygroundError(3, 'Foo', null), ]), ], ], [ [ 70100 => [ - new PlaygroundError(2, 'Foo'), - new PlaygroundError(3, 'Foo'), + new PlaygroundError(2, 'Foo', null), + new PlaygroundError(3, 'Foo', null), ], 70200 => [ - new PlaygroundError(3, 'Foo'), + new PlaygroundError(3, 'Foo', null), ], ], [ new PlaygroundResultTab('PHP 7.2 (1 error)', [ - new PlaygroundError(3, 'Foo'), + new PlaygroundError(3, 'Foo', null), ]), new PlaygroundResultTab('PHP 7.1 (2 errors)', [ - new PlaygroundError(2, 'Foo'), - new PlaygroundError(3, 'Foo'), + 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), ]), ], ], diff --git a/issue-bot/tests/PostGeneratorTest.php b/issue-bot/tests/PostGeneratorTest.php index 61e6d5145b..06c30811c9 100644 --- a/issue-bot/tests/PostGeneratorTest.php +++ b/issue-bot/tests/PostGeneratorTest.php @@ -31,10 +31,10 @@ public function dataGeneratePosts(): iterable yield [ 'abc-def', [new PlaygroundResultTab('PHP 7.1', [ - new PlaygroundError(1, 'abc'), + new PlaygroundError(1, 'abc', null), ])], [new PlaygroundResultTab('PHP 7.1', [ - new PlaygroundError(1, 'abc'), + new PlaygroundError(1, 'abc', null), ])], [], null, @@ -44,10 +44,10 @@ public function dataGeneratePosts(): iterable yield [ 'abc-def', [new PlaygroundResultTab('PHP 7.1', [ - new PlaygroundError(1, 'abc'), + new PlaygroundError(1, 'abc', null), ])], [new PlaygroundResultTab('PHP 7.1', [ - new PlaygroundError(1, 'def'), + new PlaygroundError(1, 'def', null), ])], [], $diff, @@ -57,10 +57,10 @@ public function dataGeneratePosts(): iterable yield [ 'abc-def', [new PlaygroundResultTab('PHP 7.1', [ - new PlaygroundError(1, 'abc'), + new PlaygroundError(1, 'abc', null), ])], [new PlaygroundResultTab('PHP 7.1', [ - new PlaygroundError(1, 'def'), + new PlaygroundError(1, 'def', null), ])], [ new BotComment('', new PlaygroundExample('', 'abc-def'), 'some diff'), @@ -72,10 +72,10 @@ public function dataGeneratePosts(): iterable yield [ 'abc-def', [new PlaygroundResultTab('PHP 7.1', [ - new PlaygroundError(1, 'abc'), + new PlaygroundError(1, 'abc', null), ])], [new PlaygroundResultTab('PHP 7.1', [ - new PlaygroundError(1, 'def'), + new PlaygroundError(1, 'def', null), ])], [ new BotComment('', new PlaygroundExample('', 'abc-def'), $diff), @@ -87,10 +87,10 @@ public function dataGeneratePosts(): iterable yield [ 'abc-def', [new PlaygroundResultTab('PHP 7.1', [ - new PlaygroundError(1, 'abc'), + new PlaygroundError(1, 'abc', null), ])], [new PlaygroundResultTab('PHP 7.1', [ - new PlaygroundError(1, 'Internal error'), + new PlaygroundError(1, 'Internal error', null), ])], [], null, 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 0c0edd590b..0000000000 --- a/patches/NameNodeMapper.patch +++ /dev/null @@ -1,30 +0,0 @@ -@package rector/rector - ---- packages/StaticTypeMapper/PhpParser/NameNodeMapper.php 2023-01-06 14:56:29.000000000 +0100 -+++ packages/StaticTypeMapper/PhpParser/NameNodeMapper.php 2023-01-06 14:57:51.000000000 +0100 -@@ -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; -@@ -102,7 +103,7 @@ - return new StaticType($classReflection); - } - if ($reference === ObjectReference::SELF) { -- return new SelfStaticType($classReflection); -+ return new ObjectType($classReflection->getName()); - } - if ($reference === ObjectReference::PARENT) { - $parentClassReflection = $classReflection->getParentClass(); -@@ -111,7 +112,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 6880ab9575..7ef56aaeb0 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -28,6 +28,7 @@ + @@ -46,6 +47,9 @@ + + src/Rules/Whitespace/FileWhitespaceRule.php + 10 @@ -76,9 +80,6 @@ tests - - src/Command/AnalyseApplication.php - diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 2fbf905400..4734db8b1a 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -32,13 +32,18 @@ parameters: path: src/Analyser/MutatingScope.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" - count: 2 + message: "#^Casting to string something that's already string\\.$#" + count: 3 path: src/Analyser/MutatingScope.php - - message: "#^Only numeric types are allowed in pre\\-decrement, bool\\|float\\|int\\|string\\|null given\\.$#" - count: 1 + 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 - @@ -47,20 +52,38 @@ parameters: path: src/Analyser/MutatingScope.php - - message: "#^Parameter \\#11 \\$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\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + 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 @@ -118,16 +141,11 @@ parameters: - 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\\(\\)\\.$#" 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 @@ -137,19 +155,19 @@ 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/ParametersSchemaExtension.php + path: src/DependencyInjection/NeonAdapter.php - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" @@ -230,7 +248,7 @@ parameters: path: src/PhpDoc/Tag/VarTag.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getArrays\\(\\) instead\\.$#" + 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 @@ -250,17 +268,17 @@ parameters: path: src/PhpDoc/TypeNodeResolver.php - - message: "#^Dead catch \\- PHPStan\\\\BetterReflection\\\\Identifier\\\\Exception\\\\InvalidIdentifierName is never thrown in the try block\\.$#" - count: 2 - path: src/Reflection/BetterReflection/BetterReflectionProvider.php + 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\\\\NodeCompiler\\\\Exception\\\\UnableToCompileNode\\|PHPStan\\\\BetterReflection\\\\Reflection\\\\Exception\\\\NotAClassReflection\\|PHPStan\\\\BetterReflection\\\\Reflection\\\\Exception\\\\NotAnInterfaceReflection is never thrown in the try block\\.$#" - count: 1 + 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 \\#11 \\$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 @@ -292,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 @@ -317,6 +340,16 @@ parameters: count: 1 path: src/Reflection/BetterReflection/SourceLocator/SkipClassAliasSourceLocator.php + - + 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: "#^Method PHPStan\\\\Reflection\\\\ClassReflection\\:\\:getCacheKey\\(\\) should return string but returns string\\|null\\.$#" count: 1 @@ -370,11 +403,21 @@ 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 @@ -405,6 +448,61 @@ parameters: 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 class PHPStan\\\\Analyser\\\\Scope\\: @@ -418,20 +516,65 @@ parameters: 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\\(\\) instead\\.$#" + 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\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#" + 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/DeadCode/UnusedPrivateMethodRule.php + 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\\.$#" @@ -458,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 @@ -484,7 +637,7 @@ parameters: path: src/Rules/LazyRegistry.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getArrays\\(\\) instead\\.$#" + message: "#^Doing instanceof PHPStan\\\\Type\\\\ArrayType is error\\-prone and deprecated\\. Use Type\\:\\:isArray\\(\\) or Type\\:\\:getArrays\\(\\) instead\\.$#" count: 1 path: src/Rules/Methods/MethodParameterComparisonHelper.php @@ -493,6 +646,36 @@ parameters: count: 1 path: src/Rules/Methods/MethodParameterComparisonHelper.php + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericClassStringType is error\\-prone and deprecated\\. Use Type\\:\\:isClassStringType\\(\\) and Type\\:\\:getClassStringObjectType\\(\\) instead\\.$#" + count: 1 + path: src/Rules/Methods/StaticMethodCallCheck.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" + count: 1 + path: src/Rules/PhpDoc/RequireExtendsCheck.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" + count: 1 + path: src/Rules/PhpDoc/RequireImplementsDefinitionTraitRule.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\Generic\\\\GenericObjectType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Rules/PhpDoc/VarTagTypeRuleHelper.php + + - + message: "#^Access to an undefined property PHPStan\\\\Rules\\\\RuleError\\:\\:\\$tip\\.$#" + count: 2 + path: src/Rules/RuleErrorBuilder.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Rules/RuleLevelHelper.php + - message: "#^Doing instanceof PHPStan\\\\Type\\\\NullType is error\\-prone and deprecated\\. Use Type\\:\\:isNull\\(\\) instead\\.$#" count: 2 @@ -503,6 +686,11 @@ parameters: count: 1 path: src/Rules/RuleLevelHelper.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/TooWideTypehints/TooWideMethodReturnTypehintRule.php + - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" count: 1 @@ -523,23 +711,88 @@ parameters: count: 1 path: src/Testing/PHPStanTestCase.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: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 1 + path: src/Type/Accessory/AccessoryArrayListType.php + + - + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" + count: 1 + 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\\\\ArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getArrays\\(\\) instead\\.$#" + 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 @@ -553,19 +806,44 @@ parameters: 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\\\\ArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getArrays\\(\\) instead\\.$#" + 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 - @@ -575,11 +853,11 @@ parameters: - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" - count: 7 + count: 3 path: src/Type/Constant/ConstantArrayType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\StringType is error\\-prone and deprecated\\. Use Type\\:\\:isString\\(\\) instead\\.$#" + message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#" count: 1 path: src/Type/Constant/ConstantArrayType.php @@ -588,6 +866,11 @@ parameters: 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 @@ -603,11 +886,31 @@ parameters: 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 @@ -618,11 +921,21 @@ parameters: 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 @@ -644,12 +957,22 @@ parameters: path: src/Type/Enum/EnumCaseObjectType.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\FloatType is error\\-prone and deprecated\\. Use Type\\:\\:isFloat\\(\\) instead\\.$#" + 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/FloatType.php + path: src/Type/ExponentiateHelper.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ClassStringType is error\\-prone and deprecated\\. Use Type\\:\\:isClassStringType\\(\\) instead\\.$#" + 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 @@ -658,28 +981,138 @@ parameters: 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\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" + 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\\\\TypeWithClassName is error\\-prone and deprecated\\. Use Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" + 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\\\\ArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getArrays\\(\\) instead\\.$#" + 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 @@ -703,11 +1136,26 @@ parameters: 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 @@ -723,16 +1171,31 @@ parameters: 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 @@ -740,7 +1203,7 @@ parameters: - message: "#^Doing instanceof PHPStan\\\\Type\\\\Accessory\\\\AccessoryType is error\\-prone and deprecated\\. Use methods on PHPStan\\\\Type\\\\Type instead\\.$#" - count: 2 + count: 3 path: src/Type/IntersectionType.php - @@ -753,16 +1216,62 @@ parameters: 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\\: @@ -784,6 +1293,11 @@ parameters: 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 @@ -794,11 +1308,21 @@ parameters: 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 @@ -809,6 +1333,11 @@ parameters: 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 @@ -819,6 +1348,16 @@ parameters: 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 @@ -845,10 +1384,15 @@ parameters: path: src/Type/Php/DefinedConstantTypeSpecifyingExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\TypeWithClassName is error\\-prone and deprecated\\. Use Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" + 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 @@ -859,11 +1403,37 @@ parameters: 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\\: @@ -872,6 +1442,11 @@ parameters: 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 @@ -897,11 +1472,26 @@ parameters: 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 @@ -925,10 +1515,10 @@ parameters: - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" count: 1 - path: src/Type/Php/ReflectionGetAttributesMethodReturnTypeExtension.php + path: src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) 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/Php/ReflectionMethodConstructorThrowTypeExtension.php @@ -937,6 +1527,11 @@ parameters: 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 @@ -949,7 +1544,7 @@ parameters: - message: "#^Doing instanceof PHPStan\\\\Type\\\\ObjectType is error\\-prone and deprecated\\. Use Type\\:\\:isObject\\(\\) or Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" - count: 3 + count: 2 path: src/Type/StaticType.php - @@ -973,7 +1568,7 @@ parameters: path: src/Type/TypeCombinator.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\ArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getArrays\\(\\) instead\\.$#" + 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 @@ -992,9 +1587,14 @@ parameters: 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: 12 + count: 10 path: src/Type/TypeCombinator.php - @@ -1002,16 +1602,31 @@ parameters: 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 @@ -1022,13 +1637,18 @@ parameters: 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\\:\\:getArrays\\(\\) instead\\.$#" + 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 @@ -1038,12 +1658,17 @@ parameters: path: src/Type/TypeUtils.php - - message: "#^Doing instanceof PHPStan\\\\Type\\\\TypeWithClassName is error\\-prone and deprecated\\. Use Type\\:\\:getObjectClassNames\\(\\) instead\\.$#" + 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\\:\\:getArrays\\(\\) instead\\.$#" + 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 @@ -1057,39 +1682,59 @@ parameters: 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\\\\Constant\\\\ConstantStringType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantStrings\\(\\) instead\\.$#" + 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\\\\IntegerType is error\\-prone and deprecated\\. Use Type\\:\\:isInteger\\(\\) instead\\.$#" + 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\\\\NullType is error\\-prone and deprecated\\. Use Type\\:\\:isNull\\(\\) instead\\.$#" + 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: 1 - path: src/Type/VerbosityLevel.php + count: 2 + path: src/Type/UnionTypeHelper.php - message: "#^Doing instanceof PHPStan\\\\Type\\\\VoidType is error\\-prone and deprecated\\. Use Type\\:\\:isVoid\\(\\) instead\\.$#" - count: 3 + count: 2 path: src/Type/VoidType.php - @@ -1147,6 +1792,11 @@ parameters: 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 @@ -1184,3 +1834,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 b1f760dea7..df3ec0da7b 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -259,7 +259,7 @@ 'AppendIterator::rewind' => ['void'], 'AppendIterator::valid' => ['bool'], 'array_change_key_case' => ['array', 'input'=>'array', 'case='=>'int'], -'array_chunk' => ['list', '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'], @@ -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'], @@ -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='=>''], @@ -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'], @@ -1644,9 +1645,9 @@ 'DateTimeZone::getTransitions' => ['list', 'timestamp_begin='=>'int', 'timestamp_end='=>'int'], 'DateTimeZone::listAbbreviations' => ['array>'], 'DateTimeZone::listIdentifiers' => ['list', 'what='=>'int', 'country='=>'string'], -'db2_autocommit' => ['mixed', 'connection'=>'resource', 'value='=>'int'], +'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'], @@ -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'], @@ -2638,7 +2639,7 @@ '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'], @@ -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,8 +3000,8 @@ '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'], @@ -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'], @@ -3310,7 +3311,7 @@ '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'], @@ -3330,8 +3331,8 @@ 'gethostbyname' => ['string', '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'], @@ -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' => ['list'], +'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' => ['list'], +'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'], @@ -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' => ['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{width:0|positive-int,height:0|positive-int}', '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,11 +4875,11 @@ '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::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'], @@ -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,12 +5157,11 @@ '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' => ['list>'], -'ImagickKernel::scale' => ['void'], +'ImagickKernel::scale' => ['void', 'scale'=>'float', 'normalizeFlag'=>'int'], 'ImagickKernel::separate' => ['array'], -'ImagickKernel::seperate' => ['void'], 'ImagickPixel::__construct' => ['void', 'color='=>'string'], 'ImagickPixel::clear' => ['bool'], 'ImagickPixel::clone' => ['void'], @@ -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'], @@ -5699,7 +5700,7 @@ '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' => ['non-empty-string|false', 'data'=>'mixed', 'options='=>'int', 'depth='=>'positive-int'], -'json_last_error' => ['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'], @@ -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|list', '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' => ['list|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' => ['list'], +'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' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'readPreference='=>'MongoDB\Driver\ReadPreference'], -'MongoDB\Driver\Server::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace'=>'string', 'query'=>'MongoDB\Driver\Query', 'readPreference='=>'MongoDB\Driver\ReadPreference'], -'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'], @@ -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_cert_locations' => ['array'], +'openssl_get_cipher_methods' => ['list', 'aliases='=>'bool'], 'openssl_get_curve_names' => ['list|false'], -'openssl_get_md_methods' => ['array', 'aliases='=>'bool'], +'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'], @@ -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'], @@ -8846,7 +9084,7 @@ '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', 'persistent_id='=>'?string', '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' => ['list', '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' => ['list|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' => ['list', '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' => ['list', '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'], @@ -9757,8 +10042,8 @@ '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'], @@ -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'], @@ -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'], @@ -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'], @@ -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'], @@ -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'], @@ -13269,88 +13554,108 @@ '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' => ['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'], @@ -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='http://github.com/=%3E'?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'], @@ -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 3b51107441..ec52b32bb1 100644 --- a/resources/functionMap_php74delta.php +++ b/resources/functionMap_php74delta.php @@ -41,7 +41,6 @@ 'get_mangled_object_vars' => ['array', 'obj'=>'object'], 'mb_str_split' => ['list|false', 'str'=>'string', 'split_length='=>'int', 'encoding='=>'string'], 'password_algos' => ['list'], - 'password_hash' => ['string|false', 'password'=>'string', 'algo'=>'string|null', 'options='=>'array'], '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 14c4f91172..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,19 +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' => ['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'], @@ -75,13 +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_encoding_aliases' => ['list', '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[]'], @@ -91,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-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'], @@ -106,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'], @@ -156,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'], @@ -170,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'], @@ -183,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'], @@ -207,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 3aa1fd948c..6054b7a9ce 100644 --- a/resources/functionMap_php82delta.php +++ b/resources/functionMap_php82delta.php @@ -21,6 +21,8 @@ */ return [ 'new' => [ + '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 a23e1f989b..55f2ac4419 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], @@ -653,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], @@ -764,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], @@ -850,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], @@ -1229,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], @@ -1292,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], @@ -1362,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], @@ -1460,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 8aaa6aa588..9000b2ffd0 100644 --- a/src/Analyser/Analyser.php +++ b/src/Analyser/Analyser.php @@ -88,7 +88,7 @@ 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); if ($internalErrorsCount >= $this->internalErrorsCountLimit) { diff --git a/src/Analyser/AnalyserResult.php b/src/Analyser/AnalyserResult.php index 692fcdf4ce..75b89e0fd1 100644 --- a/src/Analyser/AnalyserResult.php +++ b/src/Analyser/AnalyserResult.php @@ -9,18 +9,18 @@ class AnalyserResult { - /** @var list */ - private array $unorderedErrors; + /** @var list|null */ + private ?array $errors = null; /** - * @param list $errors + * @param list $unorderedErrors * @param list $collectedData * @param list $internalErrors * @param array>|null $dependencies * @param array> $exportedNodes */ public function __construct( - private array $errors, + private array $unorderedErrors, private array $internalErrors, private array $collectedData, private ?array $dependencies, @@ -29,20 +29,6 @@ public function __construct( 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(), - ], - ); } /** @@ -58,6 +44,22 @@ public function getUnorderedErrors(): array */ 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; } diff --git a/src/Analyser/ArgumentsNormalizer.php b/src/Analyser/ArgumentsNormalizer.php index eeab6bf83c..55d38ec042 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,7 +245,10 @@ 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 @@ -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/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/DirectInternalScopeFactory.php b/src/Analyser/DirectInternalScopeFactory.php index 870016f326..07d7fbe09c 100644 --- a/src/Analyser/DirectInternalScopeFactory.php +++ b/src/Analyser/DirectInternalScopeFactory.php @@ -3,12 +3,14 @@ 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; @@ -26,6 +28,7 @@ public function __construct( private ReflectionProvider $reflectionProvider, private InitializerExprTypeResolver $initializerExprTypeResolver, private DynamicReturnTypeExtensionRegistryProvider $dynamicReturnTypeExtensionRegistryProvider, + private ExpressionTypeResolverExtensionRegistryProvider $expressionTypeResolverExtensionRegistryProvider, private ExprPrinter $exprPrinter, private TypeSpecifier $typeSpecifier, private PropertyReflectionFinder $propertyReflectionFinder, @@ -43,7 +46,7 @@ public function __construct( * @param array $expressionTypes * @param array $nativeExpressionTypes * @param array $conditionalExpressions - * @param array<(FunctionReflection|MethodReflection)> $inFunctionCallsStack + * @param list $inFunctionCallsStack * @param array $currentlyAssignedExpressions * @param array $currentlyAllowedUndefinedExpressions */ @@ -76,6 +79,7 @@ public function create( $this->reflectionProvider, $this->initializerExprTypeResolver, $this->dynamicReturnTypeExtensionRegistryProvider->getRegistry(), + $this->expressionTypeResolverExtensionRegistryProvider->getRegistry(), $this->exprPrinter, $this->typeSpecifier, $this->propertyReflectionFinder, 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/FileAnalyser.php b/src/Analyser/FileAnalyser.php index f27181ccd7..69402c3de7 100644 --- a/src/Analyser/FileAnalyser.php +++ b/src/Analyser/FileAnalyser.php @@ -2,18 +2,16 @@ 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; @@ -29,7 +27,6 @@ use function restore_error_handler; use function set_error_handler; use function sprintf; -use function strpos; use const E_DEPRECATED; class FileAnalyser @@ -73,18 +70,22 @@ public function analyseFile( 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); } @@ -104,7 +105,7 @@ public function analyseFile( } 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'); continue; - } catch (UnableToCompileNode | NotAClassReflection | NotAnInterfaceReflection | CircularReference $e) { + } catch (UnableToCompileNode | CircularReference $e) { $fileErrors[] = new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, $node->getLine(), $e); continue; } @@ -114,18 +115,6 @@ public function analyseFile( } } - 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]); - } - } - foreach ($collectorRegistry->getCollectors($nodeType) as $collector) { try { $collectedData = $collector->processNode($node, $scope); @@ -140,7 +129,7 @@ public function analyseFile( } 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'); continue; - } catch (UnableToCompileNode | NotAClassReflection | NotAnInterfaceReflection | CircularReference $e) { + } catch (UnableToCompileNode | CircularReference $e) { $fileErrors[] = new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, $node->getLine(), $e); continue; } @@ -168,7 +157,7 @@ public function analyseFile( // pass } catch (IdentifierNotFound) { // pass - } catch (UnableToCompileNode | NotAClassReflection | NotAnInterfaceReflection) { + } catch (UnableToCompileNode) { // pass } }; @@ -180,7 +169,6 @@ public function analyseFile( $scope, $nodeCallback, ); - $unmatchedLineIgnores = $linesToIgnore; foreach ($temporaryFileErrors as $tmpFileError) { $line = $tmpFileError->getLine(); if ( @@ -219,7 +207,7 @@ public function analyseFile( } } } 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); } catch (ParserErrorsException $e) { foreach ($e->getErrors() as $error) { $fileErrors[] = new Error($error->getMessage(), $e->getParsedFile() ?? $file, $error->getStartLine() !== -1 ? $error->getStartLine() : null, $e); @@ -228,7 +216,7 @@ public function analyseFile( $fileErrors[] = new Error($e->getMessage(), $file, null, $e, null, null, $e->getTip()); } 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) { + } catch (UnableToCompileNode | CircularReference $e) { $fileErrors[] = new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, null, $e); } } elseif (is_dir($file)) { @@ -244,74 +232,26 @@ public function analyseFile( 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 ($node->getComments() as $comment) { - $line = $this->findLineToIgnoreComment($comment); - if ($line === null) { - continue; - } - - $lines[] = $line; - } - - return $lines; - } - /** * @param Node[] $nodes - * @return array> + * @return array */ - private function getLinesToIgnoreFromTokens(string $file, array $nodes): array + private function getLinesToIgnoreFromTokens(array $nodes): array { if (!isset($nodes[0])) { return []; } - /** @var int[] $tokenLines */ + /** @var array|null> $tokenLines */ $tokenLines = $nodes[0]->getAttribute('linesToIgnore', []); $lines = []; - foreach ($tokenLines as $tokenLine) { - $lines[$file][$tokenLine] = true; + foreach (array_keys($tokenLines) as $tokenLine) { + $lines[$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; - } - /** * @param array $analysedFiles */ diff --git a/src/Analyser/InternalScopeFactory.php b/src/Analyser/InternalScopeFactory.php index dcaf118ed9..7d3414f297 100644 --- a/src/Analyser/InternalScopeFactory.php +++ b/src/Analyser/InternalScopeFactory.php @@ -4,6 +4,7 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; interface InternalScopeFactory @@ -16,7 +17,7 @@ interface InternalScopeFactory * @param list $inClosureBindScopeClasses * @param array $currentlyAssignedExpressions * @param array $currentlyAllowedUndefinedExpressions - * @param array $inFunctionCallsStack + * @param list $inFunctionCallsStack */ public function create( ScopeContext $context, diff --git a/src/Analyser/LazyInternalScopeFactory.php b/src/Analyser/LazyInternalScopeFactory.php index 7966f61fab..0d74666e85 100644 --- a/src/Analyser/LazyInternalScopeFactory.php +++ b/src/Analyser/LazyInternalScopeFactory.php @@ -4,11 +4,13 @@ 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; @@ -40,8 +42,7 @@ public function __construct( * @param array $conditionalExpressions * @param array $currentlyAssignedExpressions * @param array $currentlyAllowedUndefinedExpressions - * @param array<(FunctionReflection|MethodReflection)> $inFunctionCallsStack - * + * @param list $inFunctionCallsStack */ public function create( ScopeContext $context, @@ -72,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), diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 9f464c1c7c..2c1a64098e 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -29,12 +29,19 @@ 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\IssetExpr; use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Parser\ArrayMapArgVisitor; use PHPStan\Parser\NewAssignedToPropertyVisitor; @@ -71,8 +78,10 @@ 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\ConditionalTypeForParameter; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; @@ -81,21 +90,25 @@ 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\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\StringType; @@ -109,6 +122,7 @@ use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use PHPStan\Type\VoidType; +use stdClass; use Throwable; use function abs; use function array_key_exists; @@ -118,11 +132,13 @@ 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_numeric; use function is_string; use function ltrim; use function sprintf; @@ -137,6 +153,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 = []; @@ -159,13 +179,14 @@ class MutatingScope implements Scope * @param array $currentlyAssignedExpressions * @param array $currentlyAllowedUndefinedExpressions * @param array $nativeExpressionTypes - * @param array $inFunctionCallsStack + * @param list $inFunctionCallsStack */ public function __construct( private InternalScopeFactory $scopeFactory, private ReflectionProvider $reflectionProvider, private InitializerExprTypeResolver $initializerExprTypeResolver, private DynamicReturnTypeExtensionRegistry $dynamicReturnTypeExtensionRegistry, + private ExpressionTypeResolverExtensionRegistry $expressionTypeResolverExtensionRegistry, private ExprPrinter $exprPrinter, private TypeSpecifier $typeSpecifier, private PropertyReflectionFinder $propertyReflectionFinder, @@ -506,7 +527,7 @@ public function getVariableType(string $variableName): Type } 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()) { @@ -615,20 +636,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(); } @@ -662,19 +695,34 @@ 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(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(); } 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) { return $this->getType($node->left)->isSmallerThan($this->getType($node->right))->toBooleanType(); } @@ -714,8 +762,8 @@ private function resolveType(string $exprString, 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; } @@ -724,14 +772,7 @@ private function resolveType(string $exprString, Expr $node): Type } if ($isNull->yes()) { - if ($isFalsey->yes()) { - return false; - } - if ($isFalsey->no()) { - return true; - } - - return false; + return $isFalsey->no(); } return !$isFalsey->yes(); @@ -761,17 +802,20 @@ private function resolveType(string $exprString, Expr $node): Type || $node instanceof Node\Expr\BinaryOp\LogicalAnd ) { $leftBooleanType = $this->getType($node->left)->toBoolean(); - if ( - $leftBooleanType->isFalse()->yes() - ) { + if ($leftBooleanType->isFalse()->yes()) { return new ConstantBooleanType(false); } - $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->filterByTruthyValue($node->left)->getType($node->right)->toBoolean(); + } - if ( - $rightBooleanType->isFalse()->yes() - ) { + if ($rightBooleanType->isFalse()->yes()) { return new ConstantBooleanType(false); } @@ -790,17 +834,20 @@ private function resolveType(string $exprString, Expr $node): Type || $node instanceof Node\Expr\BinaryOp\LogicalOr ) { $leftBooleanType = $this->getType($node->left)->toBoolean(); - if ( - $leftBooleanType->isTrue()->yes() - ) { + if ($leftBooleanType->isTrue()->yes()) { return new ConstantBooleanType(true); } - $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->filterByFalseyValue($node->left)->getType($node->right)->toBoolean(); + } - if ( - $rightBooleanType->isTrue()->yes() - ) { + if ($rightBooleanType->isTrue()->yes()) { return new ConstantBooleanType(true); } @@ -1119,11 +1166,11 @@ private function resolveType(string $exprString, 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); @@ -1201,8 +1248,8 @@ private function resolveType(string $exprString, 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, [ @@ -1212,7 +1259,7 @@ private function resolveType(string $exprString, 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); } @@ -1270,13 +1317,13 @@ private function resolveType(string $exprString, 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(); @@ -1305,8 +1352,8 @@ private function resolveType(string $exprString, 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, [ @@ -1353,7 +1400,7 @@ private function resolveType(string $exprString, 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)); @@ -1371,6 +1418,30 @@ private function resolveType(string $exprString, Expr $node): Type return $this->initializerExprTypeResolver->getType($node, InitializerExprContext::fromScope($this)); } elseif ($node instanceof Object_) { $castToObject = static function (Type $type): Type { + $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; } @@ -1390,16 +1461,15 @@ private function resolveType(string $exprString, 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 { + } elseif (is_numeric($varValue)) { --$varValue; } @@ -1408,9 +1478,24 @@ private function resolveType(string $exprString, 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) { @@ -1446,6 +1531,9 @@ private function resolveType(string $exprString, 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; } @@ -1454,22 +1542,30 @@ private function resolveType(string $exprString, 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); } @@ -1483,7 +1579,7 @@ private function resolveType(string $exprString, 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; } @@ -1513,10 +1609,11 @@ private function resolveType(string $exprString, Expr $node): Type } if ($node instanceof Expr\BinaryOp\Coalesce) { - $leftType = $this->getType($node->left); + $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; } @@ -1528,9 +1625,7 @@ private function resolveType(string $exprString, Expr $node): Type return TypeCombinator::removeNull($leftType); } - $rightType = $this->filterByFalseyValue( - new BinaryOp\NotIdentical($node->left, new ConstFetch(new Name('null'))), - )->getType($node->right); + $rightType = $this->filterByFalseyValue($issetLeftExpr)->getType($node->right); if ($result === null) { return TypeCombinator::union( @@ -1591,35 +1686,38 @@ private function resolveType(string $exprString, 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->isTrue()->yes()) { - return $this->filterByTruthyValue($node->cond)->getType($node->cond); + return $condResult->getTruthyScope()->getType($node->cond); } if ($booleanConditionType->isFalse()->yes()) { - return $this->filterByFalseyValue($node->cond)->getType($node->else); + return $condResult->getFalseyScope()->getType($node->else); } return TypeCombinator::union( - TypeCombinator::removeFalsey($this->filterByTruthyValue($node->cond)->getType($node->cond)), - $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->isTrue()->yes()) { - return $this->filterByTruthyValue($node->cond)->getType($node->if); + return $condResult->getTruthyScope()->getType($node->if); } if ($booleanConditionType->isFalse()->yes()) { - return $this->filterByFalseyValue($node->cond)->getType($node->else); + 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), ); } @@ -1676,6 +1774,9 @@ private function resolveType(string $exprString, 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)); } @@ -1718,21 +1819,7 @@ private function resolveType(string $exprString, Expr $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->isClassStringType()->yes()) { - return new ObjectType($type->getValue()); - } - - return $type; - }); + $staticMethodCalledOnType = TypeCombinator::removeNull($this->getType($node->class))->getObjectTypeOrClassStringObjectType(); } $returnType = $this->methodCallReturnType( @@ -1785,6 +1872,9 @@ private function resolveType(string $exprString, 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)); } @@ -1821,10 +1911,7 @@ private function resolveType(string $exprString, Expr $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( @@ -1857,6 +1944,7 @@ private function resolveType(string $exprString, Expr $node): Type $this, $node->getArgs(), $calledOnType->getCallableParametersAcceptors($this), + null, )->getReturnType(); } @@ -1869,10 +1957,20 @@ private function resolveType(string $exprString, Expr $node): Type 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) { @@ -1892,7 +1990,7 @@ private function resolveType(string $exprString, Expr $node): Type } } - return $parametersAcceptor->getReturnType(); + return $this->transformVoidToNull($parametersAcceptor->getReturnType(), $node); } return new MixedType(); @@ -1932,6 +2030,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 */ @@ -1959,17 +2076,17 @@ public function issetCheck(Expr $expr, callable $typeCallback, ?bool $result = n return $result; } elseif ($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 $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)); @@ -2063,12 +2180,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); } @@ -2105,6 +2223,7 @@ private function createFirstClassCallable(array $variants): Type $variant->isVariadic(), $variant->getTemplateTypeMap(), $variant->getResolvedTemplateTypeMap(), + $variant instanceof ParametersAcceptorWithPhpDocs ? $variant->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), ); } @@ -2117,6 +2236,14 @@ public function getNativeType(Expr $expr): Type return $this->promoteNativeTypes()->getType($expr); } + public function getKeepVoidType(Expr $node): Type + { + $clonedNode = clone $node; + $clonedNode->setAttribute(self::KEEP_VOID_ATTRIBUTE_NAME, true); + + return $this->getType($clonedNode); + } + /** * @api * @deprecated Use getNativeType() @@ -2302,6 +2429,10 @@ public function isSpecified(Expr $node): bool /** @api */ public function hasExpressionType(Expr $node): TrinaryLogic { + if ($node instanceof Variable && is_string($node->name)) { + return $this->hasVariableType($node->name); + } + $exprString = $this->getNodeKey($node); if (!isset($this->expressionTypes[$exprString])) { return TrinaryLogic::createNo(); @@ -2312,10 +2443,10 @@ public function hasExpressionType(Expr $node): TrinaryLogic /** * @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, @@ -2375,7 +2506,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; } @@ -2392,7 +2523,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 */ @@ -2402,7 +2543,7 @@ 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 */ @@ -2441,26 +2582,13 @@ public function enterTrait(ClassReflection $traitReflection): self if (count($traitNameParts) > 1) { $namespace = implode('\\', array_slice($traitNameParts, 0, -1)); } - - $traitContext = $this->context->enterTrait($traitReflection); - $classReflection = $traitContext->getClassReflection(); - if ($classReflection === null) { - throw new ShouldNotHappenException(); - } - - $thisHolder = ExpressionTypeHolder::createYes(new Variable('this'), new ThisType($classReflection, null, $traitReflection)); - $expressionTypes = $this->expressionTypes; - $expressionTypes['$this'] = $thisHolder; - $nativeExpressionTypes = $this->nativeExpressionTypes; - $nativeExpressionTypes['$this'] = $thisHolder; - return $this->scopeFactory->create( - $traitContext, + $this->context->enterTrait($traitReflection), $this->isDeclareStrictTypes(), $this->getFunction(), $namespace, - $expressionTypes, - $nativeExpressionTypes, + $this->expressionTypes, + $this->nativeExpressionTypes, [], $this->inClosureBindScopeClasses, $this->anonymousFunctionReflection, @@ -2552,7 +2680,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, ); } @@ -2631,10 +2759,39 @@ private function enterFunctionLike( bool $preserveThis, ): self { + $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()) { @@ -2646,6 +2803,10 @@ private function enterFunctionLike( $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()) { @@ -2655,6 +2816,7 @@ private function enterFunctionLike( } } $nativeExpressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($parameterNode, $nativeParameterType); + $nativeExpressionTypes[$parameterOriginalValueExprString] = ExpressionTypeHolder::createYes($parameterOriginalValueExpr, $nativeParameterType); } if ($preserveThis && array_key_exists('$this', $this->expressionTypes)) { @@ -2671,6 +2833,7 @@ private function enterFunctionLike( $this->getNamespace(), array_merge($this->getConstantTypes(), $expressionTypes), array_merge($this->getNativeConstantTypes(), $nativeExpressionTypes), + $conditionalTypes, ); } @@ -2806,7 +2969,7 @@ public function enterAnonymousFunction( true, [], [], - [], + $this->inFunctionCallsStack, false, $this, $this->nativeTypesPromoted, @@ -2935,7 +3098,7 @@ private function expressionTypeIsUnchangeable(ExpressionTypeHolder $typeHolder): && $expr->name->toLowerString() === 'function_exists' && isset($expr->getArgs()[0]) && count($this->getType($expr->getArgs()[0]->value)->getConstantStrings()) === 1 - && (new ConstantBooleanType(true))->isSuperTypeOf($type)->yes(); + && $type->isTrue()->yes(); } /** @@ -2985,7 +3148,7 @@ public function enterArrowFunction(Expr\ArrowFunction $arrowFunction, ?array $ca true, [], [], - [], + $this->inFunctionCallsStack, $scope->afterExtractCall, $scope->parentScope, $this->nativeTypesPromoted, @@ -3040,7 +3203,7 @@ private function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFu $arrowFunctionScope->nativeExpressionTypes, $arrowFunctionScope->conditionalExpressions, $arrowFunctionScope->inClosureBindScopeClasses, - null, + new TrivialParametersAcceptor(), true, [], [], @@ -3102,29 +3265,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 + { + 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 = $this->getType($iteratee); - $nativeIterateeType = $this->getNativeType($iteratee); - $scope = $this->assignVariable($valueName, $iterateeType->getIterableValueType(), $nativeIterateeType->getIterableValueType()); + $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(), $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->assignExpression( new Expr\ArrayDimFetch($iteratee, new Variable($keyName)), - $iterateeType->getIterableValueType(), - $nativeIterateeType->getIterableValueType(), + $originalScope->getIterableValueType($iterateeType), + $originalScope->getIterableValueType($nativeIterateeType), ); } @@ -3311,6 +3499,10 @@ public function assignVariable(string $variableName, Type $type, Type $nativeTyp } } + $parameterOriginalValueExprString = $this->getNodeKey(new ParameterVariableOriginalValueExpr($variableName)); + unset($scope->expressionTypes[$parameterOriginalValueExprString]); + unset($scope->nativeExpressionTypes[$parameterOriginalValueExprString]); + return $scope; } @@ -3352,7 +3544,7 @@ public function unsetExpression(Expr $expr): self return $scope->invalidateExpression($expr); } - public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType): self + public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, ?TrinaryLogic $certainty = null): self { if ($expr instanceof ConstFetch) { $loweredConstName = strtolower($expr->name->toString()); @@ -3386,6 +3578,7 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType): if ($dimType instanceof ConstantIntegerType) { $types[] = new StringType(); } + $scope = $scope->specifyExpressionType( $expr->var, TypeCombinator::intersect( @@ -3393,16 +3586,23 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType): new HasOffsetValueType($dimType, $type), ), $scope->getNativeType($expr->var), + $certainty, ); } } } + if ($certainty === null) { + $certainty = TrinaryLogic::createYes(); + } elseif ($certainty->no()) { + throw new ShouldNotHappenException(); + } + $exprString = $this->getNodeKey($expr); $expressionTypes = $scope->expressionTypes; - $expressionTypes[$exprString] = ExpressionTypeHolder::createYes($expr, $type); + $expressionTypes[$exprString] = new ExpressionTypeHolder($expr, $type, $certainty); $nativeTypes = $scope->nativeExpressionTypes; - $nativeTypes[$exprString] = ExpressionTypeHolder::createYes($expr, $nativeType); + $nativeTypes[$exprString] = new ExpressionTypeHolder($expr, $nativeType, $certainty); return $this->scopeFactory->create( $this->context, @@ -3442,6 +3642,31 @@ public function assignExpression(Expr $expr, Type $type, ?Type $nativeType = nul 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 { $expressionTypes = $this->expressionTypes; @@ -3512,7 +3737,7 @@ private function shouldInvalidateExpression(string $exprStringToInvalidate, Expr } // Variables will not contain traversable expressions. skip the NodeFinder overhead - if ($expr instanceof Variable && is_string($expr->name)) { + if ($expr instanceof Variable && is_string($expr->name) && !$requireMoreCharacters) { return $exprStringToInvalidate === $this->getNodeKey($expr); } @@ -3594,6 +3819,23 @@ private function invalidateMethodsOnExpression(Expr $expressionToInvalidate): se ); } + 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, + ); + } + private function addTypeToExpression(Expr $expr, Type $type): self { $originalExprType = $this->getType($expr); @@ -3672,7 +3914,7 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self } $typeSpecifications[] = [ 'sure' => true, - 'exprString' => $exprString, + 'exprString' => (string) $exprString, 'expr' => $expr, 'type' => $type, ]; @@ -3683,7 +3925,7 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self } $typeSpecifications[] = [ 'sure' => false, - 'exprString' => $exprString, + 'exprString' => (string) $exprString, 'expr' => $expr, 'type' => $type, ]; @@ -3703,6 +3945,23 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self foreach ($typeSpecifications as $typeSpecification) { $expr = $typeSpecification['expr']; $type = $typeSpecification['type']; + + if ($expr instanceof IssetExpr) { + $issetExpr = $expr; + $expr = $issetExpr->getExpr(); + + if ($typeSpecification['sure']) { + $scope = $scope->setExpressionCertainty( + $expr, + TrinaryLogic::createMaybe(), + ); + } else { + $scope = $scope->unsetExpression($expr); + } + + continue; + } + if ($typeSpecification['sure']) { if ($specifiedTypes->shouldOverwrite()) { $scope = $scope->assignExpression($expr, $type, $type); @@ -3715,6 +3974,7 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self $specifiedExpressions[$this->getNodeKey($expr)] = ExpressionTypeHolder::createYes($expr, $scope->getType($expr)); } + $conditions = []; foreach ($scope->conditionalExpressions as $conditionalExprString => $conditionalExpressions) { foreach ($conditionalExpressions as $conditionalExpression) { foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) { @@ -3723,18 +3983,25 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self } } - if ($conditionalExpression->getTypeHolder()->getCertainty()->no()) { - unset($scope->expressionTypes[$conditionalExprString]); - } else { - $scope->expressionTypes[$conditionalExprString] = array_key_exists($conditionalExprString, $scope->expressionTypes) - ? new ExpressionTypeHolder( - $scope->expressionTypes[$conditionalExprString]->getExpr(), - TypeCombinator::intersect($scope->expressionTypes[$conditionalExprString]->getType(), $conditionalExpression->getTypeHolder()->getType()), - TrinaryLogic::maxMin($scope->expressionTypes[$conditionalExprString]->getCertainty(), $conditionalExpression->getTypeHolder()->getCertainty()), - ) - : $conditionalExpression->getTypeHolder(); - $specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder(); - } + $conditions[$conditionalExprString][] = $conditionalExpression; + $specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder(); + } + } + + foreach ($conditions as $conditionalExprString => $expressions) { + $certainty = TrinaryLogic::lazyExtremeIdentity($expressions, static fn (ConditionalExpressionHolder $holder) => $holder->getTypeHolder()->getCertainty()); + if ($certainty->no()) { + unset($scope->expressionTypes[$conditionalExprString]); + } else { + $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(); } } @@ -3998,6 +4265,34 @@ private function mergeVariableHolders(array $ourVariableTypeHolders, array $thei return $intersectedVariableTypeHolders; } + public function mergeInitializedProperties(self $calledMethodScope): self + { + $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( @@ -4527,10 +4822,10 @@ private static function generalizeType(Type $a, Type $b, int $depth): Type TypeUtils::getAccessoryTypes($a), ); - return TypeCombinator::intersect( + return TypeCombinator::union(TypeCombinator::intersect( TypeCombinator::union(...$resultTypes, ...$otherTypes), ...$accessoryTypes, - ); + ), ...$otherTypes); } private static function getArrayDepth(Type $type): int @@ -4587,6 +4882,20 @@ private function compareVariableTypeHolders(array $variableTypeHolders, array $o 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 { @@ -4665,6 +4974,24 @@ public function debug(): array $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; } @@ -4697,6 +5024,7 @@ private function exactInstantiation(New_ $node, string $className): ?Type $this, $methodCall->getArgs(), $constructorMethod->getVariants(), + $constructorMethod->getNamedArgumentsVariants(), ); $normalizedMethodCall = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $methodCall); @@ -4771,13 +5099,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(); @@ -4806,40 +5147,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 (count($type->getConstantStrings()) > 0) { - $types = []; - foreach ($type->getConstantStrings() as $constantString) { - $types[] = new ObjectType($constantString->getValue()); - } - - return TypeCombinator::union(...$types); - } - - if ($type instanceof GenericClassStringType) { - return $type->getGenericType(); - } - - if ($type->isObject()->yes()) { - return $type; - } - - return new ObjectWithoutClassType(); - } - - /** @api */ - public function getMethodReflection(Type $typeWithMethod, string $methodName): ?ExtendedMethodReflection + private function filterTypeWithMethod(Type $typeWithMethod, string $methodName): ?Type { if ($typeWithMethod instanceof UnionType) { $newTypes = []; @@ -4860,7 +5168,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(); } /** @@ -4868,15 +5198,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); @@ -4884,7 +5216,7 @@ 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 = []; @@ -4923,10 +5255,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 */ @@ -5025,4 +5357,44 @@ private function getNativeConstantTypes(): array 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 2dd84fd5d2..a01fe0a5bf 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -45,12 +45,14 @@ 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\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 +74,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 +95,7 @@ use PHPStan\Node\InForeachNode; use PHPStan\Node\InFunctionNode; use PHPStan\Node\InstantiationCallableNode; +use PHPStan\Node\InTraitNode; use PHPStan\Node\LiteralArrayItem; use PHPStan\Node\LiteralArrayNode; use PHPStan\Node\MatchExpressionArm; @@ -99,6 +108,7 @@ 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; @@ -110,21 +120,26 @@ use PHPStan\PhpDoc\Tag\VarTag; use PHPStan\Reflection\Assertions; 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; @@ -135,20 +150,25 @@ use PHPStan\Type\ErrorType; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\GeneralizePrecision; -use PHPStan\Type\Generic\GenericClassStringType; 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\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\UnionType; use Throwable; use Traversable; @@ -157,6 +177,8 @@ 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; @@ -186,9 +208,16 @@ class NodeScopeResolver /** @var array */ 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, @@ -199,17 +228,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 = []; @@ -241,7 +275,6 @@ public function processNodes( callable $nodeCallback, ): void { - $nodesCount = count($nodes); foreach ($nodes as $i => $node) { if (!$node instanceof Node\Stmt) { continue; @@ -253,14 +286,12 @@ public function processNodes( 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; } } @@ -324,8 +355,8 @@ public function processStmtNodes( } $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; @@ -356,11 +387,6 @@ private function processStmtNode( ): StatementResult { if ( - $stmt instanceof Throw_ - || $stmt instanceof Return_ - ) { - $scope = $this->processStmtVarAnnotation($scope, $stmt, $stmt->expr, $nodeCallback); - } elseif ( !$stmt instanceof Static_ && !$stmt instanceof Foreach_ && !$stmt instanceof Node\Stmt\Global_ @@ -393,13 +419,20 @@ private function processStmtNode( } } - $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 = []; + $alwaysTerminating = false; + $exitPoints = []; foreach ($stmt->declares as $declare) { $nodeCallback($declare, $scope); $nodeCallback($declare->value, $scope); @@ -413,14 +446,25 @@ 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(); + $alwaysTerminating = $result->isAlwaysTerminating(); + $exitPoints = $result->getExitPoints(); + } + + return new StatementResult($scope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints); } elseif ($stmt instanceof Node\Stmt\Function_) { $hasYield = false; $throwPoints = []; - $this->processAttributeGroups($stmt->attrGroups, $scope, $nodeCallback); + $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); [$templateTypeMap, $phpDocParameterTypes, $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) { @@ -451,8 +495,9 @@ private function processStmtNode( $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 { + $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $functionScope, static function (Node $node, Scope $scope) use ($nodeCallback, $functionScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds): void { $nodeCallback($node, $scope); if ($scope->getFunction() !== $functionScope->getFunction()) { return; @@ -464,6 +509,9 @@ private function processStmtNode( $executionEnds[] = $node; return; } + if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) { + $gatheredYieldStatements[] = $node; + } if (!$node instanceof Return_) { return; } @@ -474,17 +522,19 @@ private function processStmtNode( $nodeCallback(new FunctionReturnStatementsNode( $stmt, $gatheredReturnStatements, + $gatheredYieldStatements, $statementResult, $executionEnds, + $functionReflection, ), $functionScope); } elseif ($stmt instanceof Node\Stmt\ClassMethod) { $hasYield = false; $throwPoints = []; - $this->processAttributeGroups($stmt->attrGroups, $scope, $nodeCallback); + $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); [$templateTypeMap, $phpDocParameterTypes, $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) { @@ -513,7 +563,8 @@ private function processStmtNode( throw new ShouldNotHappenException(); } - if ($stmt->name->toLowerString() === '__construct') { + $isFromTrait = $stmt->getAttribute('originalTraitMethodName') === '__construct'; + if ($stmt->name->toLowerString() === '__construct' || $isFromTrait) { foreach ($stmt->params as $param) { if ($param->flags === 0) { continue; @@ -534,12 +585,15 @@ 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()); } } @@ -553,8 +607,9 @@ private function processStmtNode( 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 { + $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $methodScope, static function (Node $node, Scope $scope) use ($nodeCallback, $methodScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds): void { $nodeCallback($node, $scope); if ($scope->getFunction() !== $methodScope->getFunction()) { return; @@ -566,24 +621,38 @@ private function processStmtNode( $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, + $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(); @@ -592,7 +661,7 @@ private function processStmtNode( $throwPoints = $overridingThrowPoints ?? $throwPoints; } 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(); $scope = $result->getScope(); $hasYield = $result->hasYield(); @@ -606,7 +675,7 @@ private function processStmtNode( ], $overridingThrowPoints ?? $throwPoints); } 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(); @@ -620,7 +689,7 @@ private function processStmtNode( ], $overridingThrowPoints ?? $throwPoints); } elseif ($stmt instanceof Node\Stmt\Expression) { $earlyTerminationExpr = $this->findEarlyTerminatingExpr($stmt->expr, $scope); - $result = $this->processExprNode($stmt->expr, $scope, $nodeCallback, ExpressionContext::createTopLevel()); + $result = $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createTopLevel()); $scope = $result->getScope(); $scope = $scope->filterBySpecifiedTypes($this->typeSpecifier->specifyTypesInCondition( $scope, @@ -668,20 +737,24 @@ 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, $context); - $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); + $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); + $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); foreach ($stmt->props as $prop) { - $this->processStmtNode($prop, $scope, $nodeCallback, $context); + $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(); @@ -702,11 +775,13 @@ private function processStmtNode( $docComment, $phpDocType, false, + false, $prop, $isReadOnly, $scope->isInTrait(), $scope->getClassReflection()->isReadOnly(), $isAllowedPrivateMutation, + $scope->getClassReflection(), ), $scope, ); @@ -715,14 +790,8 @@ 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); return new StatementResult($result->getScope(), $result->hasYield(), true, [ @@ -731,7 +800,7 @@ private function processStmtNode( } elseif ($stmt instanceof If_) { $conditionType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($stmt->cond) : $scope->getNativeType($stmt->cond))->toBoolean(); $ifAlwaysTrue = $conditionType->isTrue()->yes(); - $condResult = $this->processExprNode($stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep()); + $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep()); $exitPoints = []; $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints(); $finalScope = null; @@ -756,7 +825,7 @@ private function processStmtNode( foreach ($stmt->elseifs as $elseif) { $nodeCallback($elseif, $scope); $elseIfConditionType = ($this->treatPhpDocTypesAsCertain ? $condScope->getType($elseif->cond) : $scope->getNativeType($elseif->cond))->toBoolean(); - $condResult = $this->processExprNode($elseif->cond, $condScope, $nodeCallback, ExpressionContext::createDeep()); + $condResult = $this->processExprNode($stmt, $elseif->cond, $condScope, $nodeCallback, ExpressionContext::createDeep()); $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints()); $condScope = $condResult->getScope(); $branchScopeStatementResult = $this->processStmtNodes($elseif, $elseif->stmts, $condResult->getTruthyScope(), $nodeCallback, $context); @@ -818,27 +887,28 @@ private function processStmtNode( $throwPoints = []; $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(); $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); + $nodeCallback(new InForeachNode($stmt), $scope); + $originalScope = $scope; $bodyScope = $scope; if ($context->isTopLevel()) { - $bodyScope = $this->polluteScopeWithAlwaysIterableForeach ? $this->enterForeach($scope->filterByTruthyValue($arrayComparisonExpr), $stmt) : $this->enterForeach($scope, $stmt); + $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, $stmt); + $bodyScope = $this->enterForeach($bodyScope, $originalScope, $stmt); $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void { }, $context->enterDeep())->filterOutLoopExitPoints(); $bodyScope = $bodyScopeResult->getScope(); @@ -857,7 +927,7 @@ private function processStmtNode( } $bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope); - $bodyScope = $this->enterForeach($bodyScope, $stmt); + $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) { @@ -867,15 +937,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 @@ -896,7 +971,7 @@ private function processStmtNode( $throwPoints, ); } 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(); @@ -905,7 +980,7 @@ private function processStmtNode( do { $prevScope = $bodyScope; $bodyScope = $bodyScope->mergeWith($scope); - $bodyScope = $this->processExprNode($stmt->cond, $bodyScope, static function (): void { + $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(); @@ -926,7 +1001,7 @@ private function processStmtNode( $bodyScope = $bodyScope->mergeWith($scope); $bodyScopeMaybeRan = $bodyScope; - $bodyScope = $this->processExprNode($stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); + $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) { @@ -993,7 +1068,7 @@ private function processStmtNode( foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); } - $bodyScope = $this->processExprNode($stmt->cond, $bodyScope, static function (): void { + $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, static function (): void { }, ExpressionContext::createDeep())->getTruthyScope(); if ($bodyScope->equals($prevScope)) { break; @@ -1028,12 +1103,12 @@ 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(); $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); @@ -1051,7 +1126,7 @@ private function processStmtNode( $hasYield = false; $throwPoints = []; 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()); @@ -1060,7 +1135,7 @@ private function processStmtNode( $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(); $condResultScope = $condResult->getScope(); @@ -1082,7 +1157,7 @@ private function processStmtNode( $prevScope = $bodyScope; $bodyScope = $bodyScope->mergeWith($initScope); foreach ($stmt->cond as $condExpr) { - $bodyScope = $this->processExprNode($condExpr, $bodyScope, static function (): void { + $bodyScope = $this->processExprNode($stmt, $condExpr, $bodyScope, static function (): void { }, ExpressionContext::createDeep())->getTruthyScope(); } $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void { @@ -1092,7 +1167,7 @@ private function processStmtNode( $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); } foreach ($stmt->loop as $loopExpr) { - $exprResult = $this->processExprNode($loopExpr, $bodyScope, static function (): void { + $exprResult = $this->processExprNode($stmt, $loopExpr, $bodyScope, static function (): void { }, ExpressionContext::createTopLevel()); $bodyScope = $exprResult->getScope(); $hasYield = $hasYield || $exprResult->hasYield(); @@ -1112,7 +1187,7 @@ private function processStmtNode( $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, $context)->filterOutLoopExitPoints(); @@ -1123,7 +1198,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) { @@ -1161,7 +1236,7 @@ private function processStmtNode( array_merge($throwPoints, $finalScopeResult->getThrowPoints()), ); } 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; @@ -1174,11 +1249,11 @@ private function processStmtNode( 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); + $branchScope = $caseResult->getTruthyScope()->filterByTruthyValue($condExpr); } else { $hasDefaultCase = true; $branchScope = $scopeForBranches; @@ -1261,61 +1336,105 @@ private function processStmtNode( 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 = TrinaryLogic::createNo()->lazyOr( - $originalCatchType->getObjectClassNames(), - static fn (string $objectClassName) => TrinaryLogic::createFromBoolean(strtolower($objectClassName) === '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->yes()) { - 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) { @@ -1400,15 +1519,60 @@ private function processStmtNode( $throwPoints = []; 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()); + 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 = []; foreach ($stmt->uses as $use) { - $this->processStmtNode($use, $scope, $nodeCallback, $context); + $nodeCallback($use, $scope); } } elseif ($stmt instanceof Node\Stmt\Global_) { $hasYield = false; @@ -1419,7 +1583,7 @@ private function processStmtNode( throw new ShouldNotHappenException(); } $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $var); - $this->processExprNode($var, $scope, $nodeCallback, ExpressionContext::createDeep()); + $this->processExprNode($stmt, $var, $scope, $nodeCallback, ExpressionContext::createDeep()); $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var); if (!is_string($var->name)) { @@ -1436,34 +1600,29 @@ private function processStmtNode( $vars = []; foreach ($stmt->vars as $var) { - $scope = $this->processStmtNode($var, $scope, $nodeCallback, $context)->getScope(); if (!is_string($var->var->name)) { - continue; + throw new ShouldNotHappenException(); } + if ($var->default !== null) { + $this->processExprNode($stmt, $var->default, $scope, $nodeCallback, ExpressionContext::createDeep()); + } + + $scope = $scope->enterExpressionAssign($var->var); + $this->processExprNode($stmt, $var->var, $scope, $nodeCallback, ExpressionContext::createDeep()); + $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) { - $hasYield = false; - $throwPoints = []; - if (!is_string($stmt->var->name)) { - throw new ShouldNotHappenException(); - } - if ($stmt->default !== null) { - $this->processExprNode($stmt->default, $scope, $nodeCallback, ExpressionContext::createDeep()); - } - $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(), new MixedType()); } elseif ($stmt instanceof Node\Stmt\Const_) { $hasYield = false; $throwPoints = []; foreach ($stmt->consts as $const) { $nodeCallback($const, $scope); - $this->processExprNode($const->value, $scope, $nodeCallback, ExpressionContext::createDeep()); + $this->processExprNode($stmt, $const->value, $scope, $nodeCallback, ExpressionContext::createDeep()); if ($const->namespacedName !== null) { $constantName = new Name\FullyQualified($const->namespacedName->toString()); } else { @@ -1474,10 +1633,10 @@ private function processStmtNode( } elseif ($stmt instanceof Node\Stmt\ClassConst) { $hasYield = false; $throwPoints = []; - $this->processAttributeGroups($stmt->attrGroups, $scope, $nodeCallback); + $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); foreach ($stmt->consts as $const) { $nodeCallback($const, $scope); - $this->processExprNode($const->value, $scope, $nodeCallback, ExpressionContext::createDeep()); + $this->processExprNode($stmt, $const->value, $scope, $nodeCallback, ExpressionContext::createDeep()); if ($scope->getClassReflection() === null) { throw new ShouldNotHappenException(); } @@ -1487,9 +1646,22 @@ private function processStmtNode( $scope->getNativeType($const->value), ); } + } elseif ($stmt instanceof Node\Stmt\EnumCase) { + $hasYield = false; + $throwPoints = []; + $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback); + if ($stmt->expr !== null) { + $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep()); + } } elseif ($stmt instanceof Node\Stmt\Nop) { $hasYield = false; $throwPoints = $overridingThrowPoints ?? []; + } elseif ($stmt instanceof Node\Stmt\GroupUse) { + $hasYield = false; + $throwPoints = []; + foreach ($stmt->uses as $use) { + $nodeCallback($use, $scope); + } } else { $hasYield = false; $throwPoints = $overridingThrowPoints ?? []; @@ -1572,14 +1744,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()), ); } @@ -1625,6 +1801,18 @@ 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); @@ -1632,7 +1820,7 @@ private function ensureShallowNonNullability(MutatingScope $scope, Scope $origin $originalNativeType = $originalScope->getNativeType($exprToSpecify); return new EnsuredNonNullabilityResult($scope, [ - new EnsuredNonNullabilityResultExpression($exprToSpecify, $originalExprType, $originalNativeType), + new EnsuredNonNullabilityResultExpression($exprToSpecify, $originalExprType, $originalNativeType, $certainty), ]); } return new EnsuredNonNullabilityResult($scope, []); @@ -1648,7 +1836,7 @@ private function ensureShallowNonNullability(MutatingScope $scope, Scope $origin return new EnsuredNonNullabilityResult( $scope, [ - new EnsuredNonNullabilityResultExpression($exprToSpecify, $exprType, $nativeType), + new EnsuredNonNullabilityResultExpression($exprToSpecify, $exprType, $nativeType, $certainty), ], ); } @@ -1678,6 +1866,7 @@ private function revertNonNullability(MutatingScope $scope, array $specifiedExpr $specifiedExpressionResult->getExpression(), $specifiedExpressionResult->getOriginalType(), $specifiedExpressionResult->getOriginalNativeType(), + $specifiedExpressionResult->getCertainty(), ); } @@ -1738,7 +1927,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) { @@ -1753,7 +1942,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); @@ -1762,16 +1951,17 @@ private function processExprNode(Expr $expr, MutatingScope $scope, callable $nod $hasYield = false; $throwPoints = []; 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 ($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); } @@ -1784,7 +1974,7 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression ); } - $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(); $scope = $result->getScope(); @@ -1803,21 +1993,38 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression $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, $nodeCallback); + $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(), + ); + } + + return $result; + }, $expr instanceof Expr\AssignOp\Coalesce, ); $scope = $result->getScope(); @@ -1840,10 +2047,27 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression $scope, $expr->getArgs(), $nameType->getCallableParametersAcceptors($scope), + null, ); } - $nameResult = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep()); + + $nameResult = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $nameResult->getThrowPoints(); + 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()); + } $scope = $nameResult->getScope(); } elseif ($this->reflectionProvider->hasFunction($expr->name, $scope)) { $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope); @@ -1851,18 +2075,19 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression $scope, $expr->getArgs(), $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), ); } 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()); - if (isset($functionReflection)) { + if ($functionReflection !== null) { $functionThrowPoint = $this->getFunctionThrowPoint($functionReflection, $parametersAcceptor, $expr, $scope); if ($functionThrowPoint !== null) { $throwPoints[] = $functionThrowPoint; @@ -1872,7 +2097,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'), [])) @@ -1882,7 +2107,16 @@ 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 ) { @@ -1900,113 +2134,28 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression } 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 (count($callArgType->getConstantArrays()) === 1) { - $iterableValueTypes = $callArgType->getConstantArrays()[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()); - $constantArray = $constantArray->isIterableAtLeastOnce()->yes() - ? TypeCombinator::intersect($array, new NonEmptyArrayType()) - : $array; - } - - $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, $scope->getNativeType($arrayArg)); + $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()), new ArrayType(new IntegerType(), new StringType())); } - if (isset($functionReflection) && $functionReflection->getName() === 'shuffle') { + if ( + $functionReflection !== null + && $functionReflection->getName() === 'shuffle' + ) { $arrayArg = $expr->getArgs()[0]->value; $scope = $scope->assignExpression( $arrayArg, @@ -2016,7 +2165,7 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra } if ( - isset($functionReflection) + $functionReflection !== null && $functionReflection->getName() === 'array_splice' && count($expr->getArgs()) >= 1 ) { @@ -2024,7 +2173,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, @@ -2033,22 +2183,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) { @@ -2065,7 +2278,7 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra ); } - $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(); $scope = $result->getScope(); @@ -2074,12 +2287,12 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra } $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) { @@ -2087,6 +2300,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); @@ -2099,22 +2313,53 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra 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') { $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()), $scope->getNativeType($expr->var)); + $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 { @@ -2124,7 +2369,7 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); } 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( @@ -2143,13 +2388,13 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra $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()); foreach ($additionalThrowPoints as $throwPoint) { @@ -2161,7 +2406,7 @@ 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()); $scope = $result->getScope(); @@ -2176,6 +2421,7 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra $scope, $expr->getArgs(), $methodReflection->getVariants(), + $methodReflection->getNamedArgumentsVariants(), ); $methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope); @@ -2217,20 +2463,9 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra $thisTypes[] = new ObjectType($directClassName); } $thisType = TypeCombinator::union(...$thisTypes); - } elseif (count($argValueType->getConstantStrings()) > 0) { - $thisTypes = []; - foreach ($argValueType->getConstantStrings() as $constantString) { - $scopeClasses[] = $constantString->getValue(); - $thisTypes[] = new ObjectType($constantString->getValue()); - } - - $thisType = TypeCombinator::union(...$thisTypes); - } elseif ($argValueType instanceof GenericClassStringType) { - $genericClassNames = $argValueType->getGenericType()->getObjectClassNames(); - if (count($genericClassNames) > 0) { - $scopeClasses = $genericClassNames; - $thisType = $argValueType->getGenericType(); - } + } else { + $thisType = $argValueType->getClassStringObjectType(); + $scopeClasses = $thisType->getObjectClassNames(); } } $closureBindScope = $scope->enterClosureBind($thisType, $nativeThisType, $scopeClasses); @@ -2246,7 +2481,7 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra 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(); @@ -2268,29 +2503,21 @@ 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()); } 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(); $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(); $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( @@ -2304,27 +2531,23 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra $hasYield = false; $throwPoints = []; 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(); $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()); $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 = []; + return $this->processClosureNode($stmt, $expr, $scope, $nodeCallback, $context, null); } elseif ($expr instanceof Expr\ArrowFunction) { - return $this->processArrowFunctionNode($expr, $scope, $nodeCallback, $context, null); + return $this->processArrowFunctionNode($stmt, $expr, $scope, $nodeCallback, null); } 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(); $scope = $result->getScope(); @@ -2332,7 +2555,7 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra $hasYield = false; $throwPoints = []; 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(); $scope = $result->getScope(); @@ -2341,7 +2564,7 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra $hasYield = false; $throwPoints = []; 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()); $scope = $result->getScope(); @@ -2350,13 +2573,13 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra $hasYield = false; $throwPoints = []; 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(); $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()); $scope = $result->getScope(); @@ -2369,28 +2592,23 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra 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()); + $scope = $keyResult->getScope(); + } + + $valueResult = $this->processExprNode($stmt, $arrayItem->value, $scope, $nodeCallback, $context->enterDeep()); + $hasYield = $hasYield || $valueResult->hasYield(); + $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints()); + $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(); @@ -2408,8 +2626,8 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra 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(); @@ -2429,12 +2647,12 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra } 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])); @@ -2445,11 +2663,11 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra $hasYield = $condResult->hasYield() || $rightResult->hasYield(); $throwPoints = array_merge($condResult->getThrowPoints(), $rightResult->getThrowPoints()); } 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()); + $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() @@ -2460,11 +2678,11 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); } 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); $hasYield = $result->hasYield(); - $scope = $result->getScope(); + $scope = $result->getScope()->afterExtractCall(); } elseif ( $expr instanceof Expr\BitwiseNot || $expr instanceof Cast @@ -2473,27 +2691,27 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra || $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(); $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); $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); $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(); @@ -2501,7 +2719,7 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra $hasYield = false; $throwPoints = []; 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(); @@ -2509,7 +2727,7 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra } 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(); @@ -2522,7 +2740,7 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra 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()); @@ -2535,12 +2753,12 @@ 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(); 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()); @@ -2556,14 +2774,14 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra if ($expr->class instanceof Expr) { $objectClasses = $scope->getType($expr)->getObjectClassNames(); if (count($objectClasses) === 1) { - $objectExprResult = $this->processExprNode(new New_(new Name($objectClasses[0])), $scope, static function (): void { + $objectExprResult = $this->processExprNode($stmt, new New_(new Name($objectClasses[0])), $scope, static function (): void { }, $context->enterDeep()); $additionalThrowPoints = $objectExprResult->getThrowPoints(); } else { $additionalThrowPoints = [ThrowPoint::createImplicit($scope, $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(); @@ -2583,13 +2801,8 @@ 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); if ($constructorThrowPoint !== null) { $throwPoints[] = $constructorThrowPoint; @@ -2603,7 +2816,7 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra 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()); @@ -2613,7 +2826,7 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra || $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 = []; @@ -2627,10 +2840,11 @@ 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; } @@ -2641,31 +2855,38 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { 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(); $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()); $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()); $ifFalseScope = $elseResult->getScope(); - if ($ifTrueType instanceof NeverType && $ifTrueType->isExplicit()) { + $condType = $scope->getType($expr->cond); + if ($condType->isTrue()->yes()) { + $finalScope = $ifTrueScope; + } elseif ($condType->isFalse()->yes()) { $finalScope = $ifFalseScope; } else { - $ifFalseType = $ifFalseScope->getType($expr->else); - - if ($ifFalseType instanceof NeverType && $ifFalseType->isExplicit()) { - $finalScope = $ifTrueScope; + if ($ifTrueType instanceof NeverType && $ifTrueType->isExplicit()) { + $finalScope = $ifFalseScope; } else { - $finalScope = $ifTrueScope->mergeWith($ifFalseScope); + $ifFalseType = $ifFalseScope->getType($expr->else); + + if ($ifFalseType instanceof NeverType && $ifFalseType->isExplicit()) { + $finalScope = $ifTrueScope; + } else { + $finalScope = $ifTrueScope->mergeWith($ifFalseScope); + } } } @@ -2682,23 +2903,23 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { ThrowPoint::createImplicit($scope, $expr), ]; 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(); } 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()); } $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; + $matchScope = $scope->enterMatch($expr); $armNodes = []; $hasDefaultCond = false; $hasAlwaysTrueCond = false; @@ -2707,7 +2928,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $hasDefaultCond = true; $matchArmBody = new MatchExpressionArmBody($matchScope, $arm->body); $armNodes[] = new MatchExpressionArm($matchArmBody, [], $arm->getLine()); - $armResult = $this->processExprNode($arm->body, $matchScope, $nodeCallback, ExpressionContext::createTopLevel()); + $armResult = $this->processExprNode($stmt, $arm->body, $matchScope, $nodeCallback, ExpressionContext::createTopLevel()); $matchScope = $armResult->getScope(); $hasYield = $hasYield || $armResult->hasYield(); $throwPoints = array_merge($throwPoints, $armResult->getThrowPoints()); @@ -2719,12 +2940,12 @@ 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); + $armCondResult = $this->processExprNode($stmt, $armCond, $armCondScope, $nodeCallback, $deepContext); $hasYield = $hasYield || $armCondResult->hasYield(); $throwPoints = array_merge($throwPoints, $armCondResult->getThrowPoints()); $armCondExpr = new BinaryOp\Identical($expr->cond, $armCond); @@ -2734,19 +2955,33 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $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()); $armResult = $this->processExprNode( + $stmt, $arm->body, $bodyScope, $nodeCallback, @@ -2765,27 +3000,32 @@ 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(); + $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(); $throwPoints[] = ThrowPoint::createExplicit($scope, $scope->getType($expr->expr), $expr, false); } elseif ($expr instanceof FunctionCallableNode) { $throwPoints = []; $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(); } } 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(); 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()); @@ -2794,13 +3034,13 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $throwPoints = []; $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(); } 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()); @@ -2809,11 +3049,14 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $throwPoints = []; $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(); } + } elseif ($expr instanceof Node\Scalar) { + $hasYield = false; + $throwPoints = []; } else { $hasYield = false; $throwPoints = []; @@ -2828,6 +3071,170 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { ); } + 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, @@ -3057,6 +3464,7 @@ private function callNodeCallbackWithExpression( * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function processClosureNode( + Node\Stmt $stmt, Expr\Closure $expr, MutatingScope $scope, callable $nodeCallback, @@ -3065,48 +3473,18 @@ private function processClosureNode( ): ExpressionResult { 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) { @@ -3145,7 +3523,7 @@ private function processClosureNode( $scope = $scope->assignVariable($inAssignRightSideVariableName, $variableType, $variableNativeType); } } - $this->processExprNode($use, $useScope, $nodeCallback, $context); + $this->processExprNode($stmt, $use->var, $useScope, $nodeCallback, $context); if (!$use->byRef) { continue; } @@ -3166,13 +3544,18 @@ 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 { + $closureStmtsCallback = static function (Node $node, Scope $scope) use ($nodeCallback, &$executionEnds, &$gatheredReturnStatements, &$gatheredYieldStatements, &$closureScope): void { $nodeCallback($node, $scope); if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) { return; } + if ($node instanceof ExecutionEndNode) { + $executionEnds[] = $node; + return; + } if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) { $gatheredYieldStatements[] = $node; } @@ -3189,6 +3572,7 @@ private function processClosureNode( $gatheredReturnStatements, $gatheredYieldStatements, $statementResult, + $executionEnds, ), $closureScope); return new ExpressionResult($scope, false, []); @@ -3221,6 +3605,7 @@ private function processClosureNode( $gatheredReturnStatements, $gatheredYieldStatements, $statementResult, + $executionEnds, ), $closureScope); return new ExpressionResult($scope->processClosureScope($closureScope, null, $byRefUses), false, []); @@ -3230,34 +3615,55 @@ private function processClosureNode( * @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); + $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $nodeCallback, ExpressionContext::createTopLevel()); + + return new ExpressionResult($scope, false, []); + } - if ($arrowFunctionCallArgs !== null) { - $acceptors = $scope->getType($expr)->getCallableParametersAcceptors($scope); + /** + * @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(), @@ -3277,32 +3683,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; + } - $arrowFunctionScope = $scope->enterArrowFunction($expr, $callableParameters); - $arrowFunctionType = $arrowFunctionScope->getAnonymousFunctionReflection(); - if (!$arrowFunctionType instanceof ClosureType) { - throw new ShouldNotHappenException(); + $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; + } + } } - $nodeCallback(new InArrowFunctionNode($arrowFunctionType, $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); @@ -3311,7 +3741,7 @@ private function processParamNode( return; } - $this->processExprNode($param->default, $scope, $nodeCallback, ExpressionContext::createDeep()); + $this->processExprNode($stmt, $param->default, $scope, $nodeCallback, ExpressionContext::createDeep()); } /** @@ -3319,6 +3749,7 @@ private function processParamNode( * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function processAttributeGroups( + Node\Stmt $stmt, array $attrGroups, MutatingScope $scope, callable $nodeCallback, @@ -3327,7 +3758,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); @@ -3342,7 +3773,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, @@ -3351,60 +3784,64 @@ private function processArgs( ?MutatingScope $closureBindScope = null, ): ExpressionResult { - $paramOutTypes = []; if ($parametersAcceptor !== null) { $parameters = $parametersAcceptor->getParameters(); - - foreach ($parameters as $parameter) { - if (!$parameter instanceof ParameterReflectionWithPhpDocs) { - continue; - } - - if ($parameter->getOutType() === null) { - continue; - } - - $paramOutTypes[$parameter->getName()] = TemplateTypeHelper::resolveTemplateTypes($parameter->getOutType(), $parametersAcceptor->getResolvedTemplateTypeMap()); - } - } - - if ($calleeReflection !== null) { - $scope = $scope->pushInFunctionCall($calleeReflection); } $hasYield = false; $throwPoints = []; 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) { - $byRefType = new MixedType(); - $assignByReference = false; if (isset($parameters[$i])) { $assignByReference = $parameters[$i]->passedByReference()->createsNewVariable(); $parameterType = $parameters[$i]->getType(); - if (isset($paramOutTypes[$parameters[$i]->getName()])) { - $byRefType = $paramOutTypes[$parameters[$i]->getName()]; + 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 (isset($paramOutTypes[$lastParameter->getName()])) { - $byRefType = $paramOutTypes[$lastParameter->getName()]; + 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, $byRefType, 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) { @@ -3413,14 +3850,22 @@ private function processArgs( if ($arg->value instanceof Expr\Closure) { $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $context); - $result = $this->processClosureNode($arg->value, $scopeToPass, $nodeCallback, $context, $parameterType ?? null); + $result = $this->processClosureNode($stmt, $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); + $result = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $parameterType ?? null); } else { - $result = $this->processExprNode($arg->value, $scopeToPass, $nodeCallback, $context->enterDeep()); + $result = $this->processExprNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $context->enterDeep()); } $scope = $result->getScope(); + if ($assignByReference && $lookForUnset) { + $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $arg->value); + } + + if ($calleeReflection !== null) { + $scope = $scope->popInFunctionCall(); + } + $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); if ($i !== 0 || $closureBindScope === null) { @@ -3429,9 +3874,73 @@ private function processArgs( $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() + ) { + $scope = $scope->invalidateExpression($arg->value, true); + } + } elseif (!(new ResourceType())->isSuperTypeOf($argType)->no()) { + $scope = $scope->invalidateExpression($arg->value, true); + } + } } return new ExpressionResult($scope, $hasYield, $throwPoints); @@ -3443,6 +3952,7 @@ private function processArgs( */ private function processAssignVar( MutatingScope $scope, + Node\Stmt $stmt, Expr $var, Expr $assignedExpr, callable $nodeCallback, @@ -3461,10 +3971,35 @@ private function processAssignVar( $throwPoints = $result->getThrowPoints(); $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::removeFalsey($type); $falseyType = TypeCombinator::intersect($type, StaticTypeFactory::falsey()); @@ -3474,13 +4009,13 @@ private function processAssignVar( $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType); $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType); - // TODO conditional expressions for native type should be handled too + $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) { @@ -3493,7 +4028,7 @@ private function processAssignVar( $var->dim, $assignedPropertyExpr, ); - $dimExprStack[] = $var->dim; + $dimFetchStack[] = $var; $var = $var->var; } @@ -3501,7 +4036,7 @@ 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(); $scope = $result->getScope(); @@ -3512,7 +4047,16 @@ private function processAssignVar( // 2. eval dimensions $offsetTypes = []; $offsetNativeTypes = []; - foreach (array_reverse($dimExprStack) as $dimExpr) { + $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; @@ -3524,7 +4068,7 @@ private function processAssignVar( 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(); @@ -3596,7 +4140,7 @@ 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)); @@ -3612,7 +4156,7 @@ private function processAssignVar( new ObjectType(ArrayAccess::class), new NullType(), ]; - if ($offsetNativeType !== null && (new IntegerType())->isSuperTypeOf($offsetNativeType)->yes()) { + if ($offsetNativeType !== null && $offsetNativeType->isInteger()->yes()) { $types[] = new StringType(); } $offsetNativeValueType = TypeCombinator::intersect($offsetNativeValueType, TypeCombinator::union(...$types)); @@ -3622,10 +4166,14 @@ private function processAssignVar( if ($varType->isArray()->yes() || !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->yes()) { 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); + if ($var instanceof PropertyFetch && $var->name instanceof Node\Identifier && !$isAssignOp) { + $scope = $scope->assignInitializedProperty($scope->getType($var->var), $var->name->toString()); + } } $scope = $scope->assignExpression( $var, @@ -3645,13 +4193,19 @@ private function processAssignVar( } } } 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 { @@ -3660,7 +4214,7 @@ 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(); $scope = $objectResult->getScope(); @@ -3669,7 +4223,7 @@ static function (): void { 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()); $scope = $propertyNameResult->getScope(); @@ -3689,11 +4243,16 @@ static function (): void { $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 @@ -3703,6 +4262,7 @@ static function (): void { // 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 { @@ -3716,7 +4276,7 @@ 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); } @@ -3726,7 +4286,7 @@ static function (): void { $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(); $scope = $propertyNameResult->getScope(); @@ -3765,9 +4325,17 @@ 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()); + $itemScope = $keyResult->getScope(); + } + + $valueResult = $this->processExprNode($stmt, $arrayItem->value, $itemScope, $nodeCallback, $context->enterDeep()); + $hasYield = $hasYield || $valueResult->hasYield(); + $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints()); if ($arrayItem->key === null) { $dimExpr = new Node\Scalar\LNumber($i); @@ -3776,6 +4344,7 @@ static function (): void { } $result = $this->processAssignVar( $scope, + $stmt, $arrayItem->value, new GetOffsetValueTypeExpr($assignedExpr, $dimExpr), $nodeCallback, @@ -3787,6 +4356,73 @@ static function (): void { $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); } + } 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); @@ -3953,7 +4589,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 = []; @@ -3997,12 +4633,12 @@ private function processVarAnnotation(MutatingScope $scope, array $variableNames 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))) @@ -4012,6 +4648,7 @@ private function enterForeach(MutatingScope $scope, Foreach_ $stmt): MutatingSco $keyVarName = $stmt->keyVar->name; } $scope = $scope->enterForeach( + $originalScope, $stmt->expr, $stmt->valueVar->name, $keyVarName, @@ -4023,6 +4660,7 @@ private function enterForeach(MutatingScope $scope, Foreach_ $stmt): MutatingSco } else { $scope = $this->processAssignVar( $scope, + $stmt, $stmt->valueVar, new GetIterableValueTypeExpr($stmt->expr), static function (): void { @@ -4035,11 +4673,12 @@ static function (): void { 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 { @@ -4120,8 +4759,20 @@ 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); } } @@ -4135,16 +4786,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; @@ -4153,15 +4810,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, StatementContext::createTopLevel()); + + $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) { @@ -4181,6 +4846,143 @@ 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, array, ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null, Assertions, ?Type, array, array<(string|int), VarTag>, bool} */ @@ -4358,4 +5160,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/ResultCache/ResultCache.php b/src/Analyser/ResultCache/ResultCache.php index 401a0e84d6..71c12e3011 100644 --- a/src/Analyser/ResultCache/ResultCache.php +++ b/src/Analyser/ResultCache/ResultCache.php @@ -16,6 +16,7 @@ class ResultCache * @param array> $collectedData * @param array> $dependencies * @param array> $exportedNodes + * @param array $projectExtensionFiles */ public function __construct( private array $filesToAnalyse, @@ -26,6 +27,7 @@ public function __construct( private array $collectedData, private array $dependencies, private array $exportedNodes, + private array $projectExtensionFiles, ) { } @@ -88,4 +90,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 15e8484c09..0e9468b8e8 100644 --- a/src/Analyser/ResultCache/ResultCacheManager.php +++ b/src/Analyser/ResultCache/ResultCacheManager.php @@ -2,7 +2,6 @@ namespace PHPStan\Analyser\ResultCache; -use Nette\DI\Definitions\Statement; use Nette\Neon\Neon; use PHPStan\Analyser\AnalyserResult; use PHPStan\Analyser\Error; @@ -10,8 +9,10 @@ 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,19 +24,18 @@ 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; @@ -58,15 +58,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 +73,6 @@ public function __construct( private array $bootstrapFiles, private array $scanFiles, private array $scanDirectories, - private array $fileReplacements, private bool $checkDependenciesOfProjectExtensionFiles, ) { @@ -84,34 +82,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 +114,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 +123,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 +140,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 +166,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']; @@ -263,7 +262,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, $filteredCollectedData, $invertedDependenciesToReturn, $filteredExportedNodes, $data['projectExtensionFiles']); } /** @@ -274,21 +273,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 = []; @@ -336,7 +365,11 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache } $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, $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 +404,7 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache } } - $this->save($resultCache->getLastFullAnalysisTime(), $resultCacheName, $errorsByFile, $collectedDataByFile, $dependencies, $exportedNodes, $meta); + $this->save($resultCache->getLastFullAnalysisTime(), $errorsByFile, $collectedDataByFile, $dependencies, $exportedNodes, $projectExtensionFiles, $meta); if ($output->isDebug()) { $output->writeLineFormatted('Result cache is saved.'); @@ -383,7 +416,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, $freshCollectedDataByFile, $analyserResult->getDependencies(), $analyserResult->getExportedNodes(), $projectExtensionFiles); } else { if ($output->isDebug()) { $output->writeLineFormatted('Result cache was not saved because it was not requested.'); @@ -400,7 +437,27 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache $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, $collectedDataByFile, $dependencies, $exportedNodes, $projectExtensionFiles); } $flatErrors = []; @@ -529,15 +586,16 @@ private function mergeExportedNodes(ResultCache $resultCache, array $freshExport * @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 $collectedData, array $dependencies, array $exportedNodes, + array $projectExtensionFiles, array $meta, ): void { @@ -581,118 +639,86 @@ 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) . "; }, + '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; } /** @@ -744,7 +770,10 @@ private function getMeta(array $allAnalysedFiles, ?array $projectConfigArray): a unset($projectConfigArray['parameters']['internalErrorsCountLimit']); unset($projectConfigArray['parameters']['cache']); unset($projectConfigArray['parameters']['memoryLimitFile']); + unset($projectConfigArray['parameters']['pro']); unset($projectConfigArray['parametersSchema']); + + ksort($projectConfigArray); } return [ @@ -765,17 +794,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 bad37ff852..30d5caf4e8 100644 --- a/src/Analyser/Scope.php +++ b/src/Analyser/Scope.php @@ -11,7 +11,9 @@ 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; @@ -63,6 +65,10 @@ public function getMethodReflection(Type $typeWithMethod, string $methodName): ? 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; @@ -73,6 +79,8 @@ public function getType(Expr $node): Type; public function getNativeType(Expr $expr): Type; + public function getKeepVoidType(Expr $node): Type; + /** * @deprecated Use getNativeType() */ @@ -98,6 +106,12 @@ 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/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 4854505a59..1e70ccb21f 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -18,14 +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; @@ -39,12 +43,13 @@ use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ConstantScalarType; -use PHPStan\Type\ConstantType; 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; @@ -64,12 +69,11 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeTraverser; 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; @@ -181,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 ConstantScalarType - || count($exprLeftType->getEnumCases()) === 1 - || ($exprLeftType instanceof ConstantType && !$exprRightType->equals($exprLeftType) && $exprRightType->isSuperTypeOf($exprLeftType)->yes()) - ) { - $types = $this->create( - $expr->right, - $exprLeftType, - $context, - false, - $scope, - $rootExpr, - ); - } - if ( - $exprRightType instanceof ConstantScalarType - || count($exprRightType->getEnumCases()) === 1 - || ($exprRightType instanceof ConstantType && !$exprLeftType->equals($exprRightType) && $exprLeftType->isSuperTypeOf($exprRightType)->yes()) - ) { - $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( @@ -290,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->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); - } - - $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, @@ -456,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), + ); } } } @@ -475,7 +266,7 @@ public function specifyTypesInCondition( && count($expr->right->getArgs()) === 1 && $expr->right->name instanceof Name && strtolower((string) $expr->right->name) === 'strlen' - && (new IntegerType())->isSuperTypeOf($leftType)->yes() + && $leftType->isInteger()->yes() ) { if ( $context->truthy() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) @@ -615,18 +406,31 @@ public function specifyTypesInCondition( return $extension->specifyTypes($functionReflection, $expr, $scope, $context); } - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $functionReflection->getVariants()); + // lazy create parametersAcceptor, as creation can be expensive + $parametersAcceptor = null; if (count($expr->getArgs()) > 0) { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $functionReflection->getVariants(), $functionReflection->getNamedArgumentsVariants()); + $specifiedTypes = $this->specifyTypesFromConditionalReturnType($context, $expr, $parametersAcceptor, $scope); if ($specifiedTypes !== null) { return $specifiedTypes; } } - $asserts = $functionReflection->getAsserts()->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes($type, $parametersAcceptor->getResolvedTemplateTypeMap())); - $specifiedTypes = $this->specifyTypesFromAsserts($context, $expr, $asserts, $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; + } } } @@ -650,18 +454,31 @@ public function specifyTypesInCondition( } } - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $methodReflection->getVariants()); + // 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; } } - $asserts = $methodReflection->getAsserts()->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes($type, $parametersAcceptor->getResolvedTemplateTypeMap())); - $specifiedTypes = $this->specifyTypesFromAsserts($context, $expr, $asserts, $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; + } } } @@ -690,18 +507,31 @@ public function specifyTypesInCondition( } } - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $staticMethodReflection->getVariants()); + // lazy create parametersAcceptor, as creation can be expensive + $parametersAcceptor = null; if (count($expr->getArgs()) > 0) { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $staticMethodReflection->getVariants(), $staticMethodReflection->getNamedArgumentsVariants()); + $specifiedTypes = $this->specifyTypesFromConditionalReturnType($context, $expr, $parametersAcceptor, $scope); if ($specifiedTypes !== null) { return $specifiedTypes; } } - $asserts = $staticMethodReflection->getAsserts()->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes($type, $parametersAcceptor->getResolvedTemplateTypeMap())); - $specifiedTypes = $this->specifyTypesFromAsserts($context, $expr, $asserts, $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; + } } } @@ -720,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, ); @@ -742,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, ); @@ -766,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, + ); } - $vars = array_merge($vars, array_reverse($tmpVars)); + if ($isNullable && $isset === true) { + return $exprType; + } + + 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 @@ -820,68 +732,96 @@ 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()) + && !$context->null() ) { - return $this->create( - $expr->left, - new NullType(), - TypeSpecifierContext::createFalse(), - false, - $scope, - $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), @@ -891,7 +831,7 @@ public function specifyTypesInCondition( } elseif ( $expr instanceof Expr\Ternary && !$context->null() - && ((new ConstantBooleanType(false))->isSuperTypeOf($scope->getType($expr->else))->yes()) + && $scope->getType($expr->else)->isFalse()->yes() ) { $conditionExpr = $expr->cond; if ($expr->if !== null) { @@ -926,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); } @@ -943,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, @@ -953,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, @@ -1025,10 +1001,6 @@ private function specifyTypesForConstantBinaryExpression( } - if ($constantType instanceof ConstantStringType) { - return $this->specifyTypesForConstantStringBinaryExpression($exprNode, $constantType, $context, $scope, $rootExpr); - } - return null; } @@ -1087,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') { @@ -1104,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); } } @@ -1374,7 +1348,74 @@ static function (Type $type, callable $traverse) use ($templateTypeMap, &$contai /** * @return array */ - private function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes): 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 processBooleanNotSureConditionalTypes(Scope $scope, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes): array { $conditionExpressionTypes = []; foreach ($leftTypes->getSureNotTypes() as $exprString => [$expr, $type]) { @@ -1405,8 +1446,28 @@ 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, + $conditions, new ExpressionTypeHolder($expr, TypeCombinator::remove($scope->getType($expr), $type), TrinaryLogic::createYes()), ); $holders[$exprString][$holder->getKey()] = $holder; @@ -1419,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]; } @@ -1457,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; } @@ -1495,9 +1574,9 @@ private function createForExpr( { if ($scope !== null) { if ($context->true()) { - $containsNull = TypeCombinator::containsNull($type) && TypeCombinator::containsNull($scope->getType($expr)); + $containsNull = !$type->isNull()->no() && !$scope->getType($expr)->isNull()->no(); } elseif ($context->false()) { - $containsNull = !TypeCombinator::containsNull($type) && TypeCombinator::containsNull($scope->getType($expr)); + $containsNull = !TypeCombinator::containsNull($type) && !$scope->getType($expr)->isNull()->no(); } } @@ -1734,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/BrokerFactory.php b/src/Broker/BrokerFactory.php index adff2e4705..d35b68e30d 100644 --- a/src/Broker/BrokerFactory.php +++ b/src/Broker/BrokerFactory.php @@ -15,6 +15,7 @@ class BrokerFactory 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 39ec535088..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 */ @@ -66,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(); @@ -84,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 f2d81ac6c3..cc6ae03688 100644 --- a/src/Command/AnalyseApplication.php +++ b/src/Command/AnalyseApplication.php @@ -12,8 +12,6 @@ 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; @@ -25,9 +23,11 @@ use Symfony\Component\Console\Input\InputInterface; use function array_merge; use function count; +use function is_file; use function is_string; use function memory_get_peak_usage; use function microtime; +use function sha1_file; use function sprintf; class AnalyseApplication @@ -63,7 +63,8 @@ public function analyse( InputInterface $input, ): AnalysisResult { - $resultCacheManager = $this->resultCacheManagerFactory->create([]); + $isResultCacheUsed = false; + $resultCacheManager = $this->resultCacheManagerFactory->create(); $ignoredErrorHelperResult = $this->ignoredErrorHelper->initialize(); if (count($ignoredErrorHelperResult->getErrors()) > 0) { @@ -75,6 +76,7 @@ public function analyse( 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( @@ -89,10 +91,15 @@ 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->getInternalErrors(), $intermediateAnalyserResult->getCollectedData(), $intermediateAnalyserResult->getDependencies(), @@ -108,9 +115,36 @@ public function analyse( $errors = $analyserResult->getErrors(); $hasInternalErrors = count($internalErrors) > 0 || $analyserResult->hasReachedInternalErrorsCountLimit(); $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; + } + } if (!$hasInternalErrors) { - foreach ($this->getCollectedDataErrors($analyserResult->getCollectedData()) as $error) { + foreach ($this->getCollectedDataErrors($analyserResult->getCollectedData(), $onlyFiles) as $error) { $errors[] = $error; } } @@ -144,6 +178,8 @@ public function analyse( $projectConfigFile, $savedResultCache, $memoryUsageBytes, + $isResultCacheUsed, + $changedProjectExtensionFilesOutsideOfAnalysedPaths, ); } @@ -151,10 +187,10 @@ public function analyse( * @param CollectedData[] $collectedData * @return Error[] */ - private function getCollectedDataErrors(array $collectedData): array + private function getCollectedDataErrors(array $collectedData, bool $onlyFiles): array { $nodeType = CollectedDataNode::class; - $node = new CollectedDataNode($collectedData); + $node = new CollectedDataNode($collectedData, $onlyFiles); $file = 'N/A'; $scope = $this->scopeFactory->create(ScopeContext::create($file)); $errors = []; @@ -167,7 +203,7 @@ private function getCollectedDataErrors(array $collectedData): array } 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) { + } catch (UnableToCompileNode | CircularReference $e) { $errors[] = new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, $node->getLine(), $e); continue; } @@ -232,7 +268,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 2d63549d0f..ff8ff8ad45 100644 --- a/src/Command/AnalyseCommand.php +++ b/src/Command/AnalyseCommand.php @@ -3,8 +3,8 @@ 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; @@ -14,6 +14,10 @@ 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; @@ -82,6 +90,7 @@ protected function configure(): void 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'), ]); } @@ -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'); @@ -203,8 +213,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int 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, null); } @@ -215,16 +225,45 @@ 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) { - $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.'); + $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(0, null); + 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), + )); + } + + /** @var AnalyseApplication $application */ $application = $container->getByType(AnalyseApplication::class); $debug = $input->getOption('debug'); @@ -288,20 +327,49 @@ protected function execute(InputInterface $input, OutputInterface $output): int return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes()); } 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.'); + $internalErrors = array_values(array_unique($analysisResult->getInternalErrors())); + + foreach ($internalErrors as $internalError) { + $inceptionResult->getStdOutput()->writeLineFormatted($internalError); + $inceptionResult->getStdOutput()->writeLineFormatted(''); + } + + $inceptionResult->getStdOutput()->getStyle()->error(sprintf( + '%s occurred. Baseline could not be generated.', + count($internalErrors) === 1 ? 'An internal error' : 'Internal errors', + )); return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes()); } - $baselineFileDirectory = dirname($generateBaselineFile); - $baselineErrorFormatter = new BaselineNeonErrorFormatter(new ParentDirectoryRelativePathHelper($baselineFileDirectory)); + foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { + if (!$fileSpecificError->hasNonIgnorableException()) { + continue; + } - $existingBaselineContent = is_file($generateBaselineFile) ? FileReader::read($generateBaselineFile) : ''; + $inceptionResult->getStdOutput()->getStyle()->error('An internal error occurred. Baseline could not be generated.'); + + $inceptionResult->getStdOutput()->writeLineFormatted($fileSpecificError->getMessage()); + $inceptionResult->getStdOutput()->writeLineFormatted($fileSpecificError->getFile()); + $inceptionResult->getStdOutput()->writeLineFormatted(''); + + return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes()); + } $streamOutput = $this->createStreamOutput(); $errorConsoleStyle = new ErrorsConsoleStyle(new StringInput(''), $streamOutput); $baselineOutput = new SymfonyOutput($streamOutput, new SymfonyStyle($errorConsoleStyle)); - $baselineErrorFormatter->formatErrors($analysisResult, $baselineOutput, $existingBaselineContent); + $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); @@ -310,13 +378,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int throw new ShouldNotHappenException(); } - if (!is_dir($baselineFileDirectory)) { - $mkdirResult = @mkdir($baselineFileDirectory, 0644, true); - if ($mkdirResult === false) { - $inceptionResult->getStdOutput()->writeLineFormatted(sprintf('Failed to create directory "%s".', $baselineFileDirectory)); + try { + DirectoryCreator::ensureDirectoryExists($baselineFileDirectory, 0644); + } catch (DirectoryCreatorException $e) { + $inceptionResult->getStdOutput()->writeLineFormatted($e->getMessage()); - return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes()); - } + return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes()); } try { @@ -355,7 +422,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int $inceptionResult->getStdOutput()->getStyle()->warning($message . "\nSome errors could not be put into baseline. Re-run PHPStan and fix them."); } - return $inceptionResult->handleReturn(0, $analysisResult->getPeakMemoryUsageBytes()); + $exitCode = 0; + if ($failWithoutResultCache && !$analysisResult->isResultCacheUsed()) { + $exitCode = 2; + } + + return $inceptionResult->handleReturn( + $exitCode, + $analysisResult->getPeakMemoryUsageBytes(), + ); } if ($fix) { @@ -365,7 +440,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes()); } - $container->getByType(ResultCacheClearer::class)->clearTemporaryCaches(); $hasInternalErrors = $analysisResult->hasInternalErrors(); $nonIgnorableErrorsByException = []; foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { @@ -387,6 +461,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $analysisResult->getProjectConfigFile(), $analysisResult->isResultCacheSaved(), $analysisResult->getPeakMemoryUsageBytes(), + $analysisResult->isResultCacheUsed(), + $analysisResult->getChangedProjectExtensionFilesOutsideOfAnalysedPaths(), ); $stdOutput = $inceptionResult->getStdOutput(); @@ -431,7 +507,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $inceptionResult->handleReturn(0, $analysisResult->getPeakMemoryUsageBytes()); - /** @var FixerApplication $fixerApplication */ $fixerApplication = $container->getByType(FixerApplication::class); return $fixerApplication->run( @@ -449,8 +524,63 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** @var ErrorFormatter $errorFormatter */ $errorFormatter = $container->getService($errorFormatterServiceName); + $exitCode = $errorFormatter->formatErrors($analysisResult, $inceptionResult->getStdOutput()); + if ($failWithoutResultCache && !$analysisResult->isResultCacheUsed()) { + $exitCode = 2; + } + + 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)); + } + + $errorOutput->writeLineFormatted(''); + + $errorOutput->writeLineFormatted('When you edit them and re-run PHPStan, the result cache will get stale.'); + + $directoriesToAdd = []; + foreach ($projectServiceFileNamesNotInAnalysedPaths as $path) { + $directoriesToAdd[] = dirname($relativePathHelper->getRelativePath($path)); + } + + $directoriesToAdd = array_unique($directoriesToAdd); + $oneDirectory = count($directoriesToAdd) === 1; + + $errorOutput->writeLineFormatted(sprintf('Add %s to your analysed paths to get rid of this problem:', $oneDirectory ? 'this directory' : 'these directories')); + + $errorOutput->writeLineFormatted(''); + + foreach ($directoriesToAdd as $directory) { + $errorOutput->writeLineFormatted(sprintf('- %s', $directory)); + } + + $errorOutput->writeLineFormatted(''); + + $bleedingEdge = (bool) $container->getParameter('featureToggles')['projectServicesNotInAnalysedPaths']; + if ($bleedingEdge) { + return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes()); + } + + $errorOutput->getStyle()->warning('This will cause a non-zero exit code in PHPStan 2.0.'); + } + } + return $inceptionResult->handleReturn( - $errorFormatter->formatErrors($analysisResult, $inceptionResult->getStdOutput()), + $exitCode, $analysisResult->getPeakMemoryUsageBytes(), ); } diff --git a/src/Command/AnalyserRunner.php b/src/Command/AnalyserRunner.php index 5275c679aa..0c951af663 100644 --- a/src/Command/AnalyserRunner.php +++ b/src/Command/AnalyserRunner.php @@ -9,8 +9,6 @@ use PHPStan\Parallel\Scheduler; use PHPStan\Process\CpuCoreCounter; 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; @@ -42,8 +40,6 @@ public function runAnalyser( bool $debug, bool $allowParallel, ?string $projectConfigFile, - ?string $tmpFile, - ?string $insteadOfFile, InputInterface $input, ): AnalyserResult { @@ -65,39 +61,16 @@ public function runAnalyser( && $mainScript !== null && $schedule->getNumberOfProcesses() > 0 ) { - return $this->parallelAnalyser->analyse($schedule, $mainScript, $postFileCallback, $projectConfigFile, $tmpFile, $insteadOfFile, $input); + return $this->parallelAnalyser->analyse($schedule, $mainScript, $postFileCallback, $projectConfigFile, $input); } 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 9a8dae37bb..02c7de64b1 100644 --- a/src/Command/AnalysisResult.php +++ b/src/Command/AnalysisResult.php @@ -20,6 +20,7 @@ class AnalysisResult * @param list $internalErrors * @param list $warnings * @param list $collectedData + * @param array $changedProjectExtensionFilesOutsideOfAnalysedPaths */ public function __construct( array $fileSpecificErrors, @@ -31,6 +32,8 @@ public function __construct( private ?string $projectConfigFile, private bool $savedResultCache, private int $peakMemoryUsageBytes, + private bool $isResultCacheUsed, + private array $changedProjectExtensionFilesOutsideOfAnalysedPaths, ) { usort( @@ -129,4 +132,17 @@ 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..4b6dbe17a3 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 81d0b8092e..1261814cef 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -11,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; @@ -22,6 +23,8 @@ 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; @@ -39,6 +42,7 @@ 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; @@ -47,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; @@ -83,8 +86,6 @@ public static function begin( ?string $level, bool $allowXdebug, bool $debugEnabled = false, - ?string $singleReflectionFile = null, - ?string $singleReflectionInsteadOfFile = null, bool $cleanupContainerCache = true, ): InceptionResult { @@ -95,12 +96,6 @@ public static function begin( return new SymfonyOutput($symfonyErrorOutput, new SymfonyStyle(new ErrorsConsoleStyle($input, $symfonyErrorOutput))); })(); - if ($allowXdebug && !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.'); - } elseif (!$allowXdebug && 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.'); - } - if (!$allowXdebug) { $xdebug = new XdebugHandler('phpstan'); $xdebug->setPersistent(); @@ -108,6 +103,25 @@ public static function begin( unset($xdebug); } + 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.', + ); + } + } + if ($memoryLimit !== null) { if (Strings::match($memoryLimit, '#^-?\d+[kMG]?$#i') === null) { $errorOutput->writeLineFormatted(sprintf('Invalid memory limit format "%s".', $memoryLimit)); @@ -130,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; } @@ -210,6 +224,7 @@ public static function begin( $defaultParameters = [ 'rootDir' => $containerFactory->getRootDirectory(), 'currentWorkingDirectory' => $containerFactory->getCurrentWorkingDirectory(), + 'env' => getenv(), ]; if (isset($projectConfig['parameters']['tmpDir'])) { @@ -274,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(); } }; @@ -286,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()); @@ -354,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) { @@ -461,6 +473,12 @@ 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.')); + } + $tempResultCachePath = $container->getParameter('tempResultCachePath'); $createDir($tempResultCachePath); @@ -469,10 +487,13 @@ public static function begin( $pathRoutingParser = $container->getService('pathRoutingParser'); - /** @var StubFilesProvider $stubFilesProvider */ $stubFilesProvider = $container->getByType(StubFilesProvider::class); - $filesCallback = static function () use ($currentWorkingDirectoryFileHelper, $stubFilesProvider, $fileFinder, $pathRoutingParser, $paths): array { + $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(); 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..9d453b5bf1 --- /dev/null +++ b/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php @@ -0,0 +1,80 @@ +hasErrors()) { + $php = 'writeRaw($php); + return 0; + } + + $fileErrors = []; + foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { + if (!$fileSpecificError->canBeIgnored()) { + continue; + } + $fileErrors['/' . $this->relativePathHelper->getRelativePath($fileSpecificError->getFilePath())][] = $fileSpecificError->getMessage(); + } + ksort($fileErrors, SORT_STRING); + + $php = ' $errorMessages) { + $fileErrorsCounts = []; + foreach ($errorMessages as $errorMessage) { + if (!isset($fileErrorsCounts[$errorMessage])) { + $fileErrorsCounts[$errorMessage] = 1; + continue; + } + + $fileErrorsCounts[$errorMessage]++; + } + ksort($fileErrorsCounts, SORT_STRING); + + foreach ($fileErrorsCounts as $message => $count) { + $php .= sprintf( + "\$ignoreErrors[] = [\n\t'message' => %s,\n\t'count' => %d,\n\t'path' => __DIR__ . %s,\n];\n", + 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/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 2fddee4cef..9f9f1edcfb 100644 --- a/src/Command/ErrorFormatter/JsonErrorFormatter.php +++ b/src/Command/ErrorFormatter/JsonErrorFormatter.php @@ -49,6 +49,10 @@ public function formatErrors(AnalysisResult $analysisResult, Output $output): in $message['tip'] = $tipFormatter->format($fileSpecificError->getTip()); } + if ($fileSpecificError->getIdentifier() !== null) { + $message['identifier'] = $fileSpecificError->getIdentifier(); + } + $errorsArray['files'][$file]['messages'][] = $message; } diff --git a/src/Command/ErrorsConsoleStyle.php b/src/Command/ErrorsConsoleStyle.php index ee19656e27..520ce7add3 100644 --- a/src/Command/ErrorsConsoleStyle.php +++ b/src/Command/ErrorsConsoleStyle.php @@ -104,8 +104,6 @@ private function wrap(array $rows, int $terminalWidth, int $maxHeaderWidth): arr $wrapped = wordwrap( $columnRow, $terminalWidth - $maxHeaderWidth - 5, - "\n", - true, ); if (str_starts_with($columnRow, '💡 ')) { $wrappedLines = explode("\n", $wrapped); diff --git a/src/Command/FixerApplication.php b/src/Command/FixerApplication.php index 6b23b5931c..ccf2c0a9bd 100644 --- a/src/Command/FixerApplication.php +++ b/src/Command/FixerApplication.php @@ -13,25 +13,21 @@ 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\File\FileMonitor; use PHPStan\File\FileMonitorResult; use PHPStan\File\FileReader; use PHPStan\File\FileWriter; +use PHPStan\File\PathNotFoundException; 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\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; @@ -57,18 +53,14 @@ 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 memory_get_peak_usage; -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 PHP_BINARY; use const PHP_URL_PORT; @@ -80,22 +72,18 @@ class FixerApplication /** @var (ExtendedPromiseInterface&CancellablePromiseInterface)|null */ private $processInProgress; - private ?string $fixerSuggestionId = null; - /** * @param string[] $analysedPaths + * @param list $dnsServers */ public function __construct( private FileMonitor $fileMonitor, private ResultCacheManagerFactory $resultCacheManagerFactory, - private ResultCacheClearer $resultCacheClearer, private IgnoredErrorHelper $ignoredErrorHelper, - private CpuCoreCounter $cpuCoreCounter, - private Scheduler $scheduler, private array $analysedPaths, private string $currentWorkingDirectory, - private string $fixerTmpDir, - private int $maximumNumberOfProcesses, + private string $proTmpDir, + private array $dnsServers, ) { } @@ -123,18 +111,7 @@ public function run( /** @var int<0, 65535> $serverPort */ $serverPort = parse_url(http://github.com/$serverAddress,%20PHP_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, $fileSpecificErrors, $notFileSpecificErrors, $mainScript, $filesCount, $inceptionResult): void { // phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly $jsonInvalidUtf8Ignore = defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0; // phpcs:enable @@ -149,61 +126,18 @@ public function log(string $message): void 'filesCount' => $filesCount, 'phpstanVersion' => ComposerHelper::getPhpStanVersion(), ]]); - $decoder->on('data', function (array $data) use ( - $loop, - $encoder, - $projectConfigFile, - $input, + $decoder->on('data', static function (array $data) use ( $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'] !== 'reanalyse') { - return; - } - - $id = $data['id']; - - $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->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, $inceptionResult): void { if ($this->processInProgress !== null) { $this->processInProgress->cancel(); $this->processInProgress = null; @@ -216,20 +150,16 @@ public function log(string $message): void $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(), @@ -254,7 +184,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(); @@ -267,20 +197,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(); } @@ -308,6 +239,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']; @@ -319,7 +251,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 { @@ -335,12 +267,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( @@ -350,21 +282,29 @@ private function downloadPhar( ): void { $currentVersion = null; + $branch = 'master'; 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( [ @@ -372,7 +312,7 @@ private function downloadPhar( 'tls' => [ 'cafile' => CaBundle::getBundledCaBundlePath(), ], - 'dns' => '1.1.1.1', + 'dns' => $dnsConfig, ], ), ); @@ -380,9 +320,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; } @@ -411,8 +351,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(); @@ -423,13 +363,27 @@ private function downloadPhar( $output->writeln(''); $output->writeln(''); - $this->writeInfoFile($infoPath, $latestInfo['version']); + $this->writeInfoFile($infoPath, $latestInfo['version'], $branch); } - private function writeInfoFile(string $infoPath, string $version): void + 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, string $branch): void { FileWriter::write($infoPath, Json::encode([ 'version' => $version, + 'branch' => $branch, 'date' => (new DateTimeImmutable('', new DateTimeZone('UTC')))->format(DateTime::ATOM), ])); } @@ -451,48 +405,11 @@ private function monitorFileChanges(LoopInterface $loop, callable $hasChangesCal $loop->addTimer(1.0, $callback); } - private function reanalyseWithTmpFile( - LoopInterface $loop, - InceptionResult $inceptionResult, - string $mainScript, - RunnableQueue $runnableQueue, - ?string $projectConfigFile, - string $tmpFile, - string $insteadOfFile, - string $fixerSuggestionId, - InputInterface $input, - ): PromiseInterface - { - $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()); - - $process = new ProcessPromise($loop, $fixerSuggestionId, ProcessHelper::getWorkerCommand( - $mainScript, - 'fixer:worker', - $projectConfigFile, - [ - '--tmp-file', - escapeshellarg($tmpFile), - '--instead-of', - escapeshellarg($insteadOfFile), - '--save-result-cache', - escapeshellarg($fixerSuggestionId), - '--allow-parallel', - ], - $input, - )); - - return $runnableQueue->queue($process, $schedule->getNumberOfProcesses()); - } - private function reanalyseAfterFileChanges( LoopInterface $loop, InceptionResult $inceptionResult, string $mainScript, ?string $projectConfigFile, - ?string $fixerSuggestionId, InputInterface $input, ): PromiseInterface { @@ -503,9 +420,15 @@ private function reanalyseAfterFileChanges( $projectConfigArray = $inceptionResult->getProjectConfigArray(); - $resultCacheManager = $this->resultCacheManagerFactory->create([]); - [$inceptionFiles, $isOnlyFiles] = $inceptionResult->getFiles(); - $resultCache = $resultCacheManager->restore($inceptionFiles, false, false, $projectConfigArray, $inceptionResult->getErrorOutput(), $fixerSuggestionId); + $resultCacheManager = $this->resultCacheManagerFactory->create(); + + try { + [$inceptionFiles, $isOnlyFiles] = $inceptionResult->getFiles(); + } catch (InceptionNotSuccessfulException | PathNotFoundException) { + throw new ShouldNotHappenException(); + } + + $resultCache = $resultCacheManager->restore($inceptionFiles, false, false, $projectConfigArray, $inceptionResult->getErrorOutput()); if (count($resultCache->getFilesToAnalyse()) === 0) { $result = $resultCacheManager->process( new AnalyserResult([], [], [], [], [], false, memory_get_peak_usage(true)), @@ -538,10 +461,6 @@ private function reanalyseAfterFileChanges( } $options = ['--save-result-cache', '--allow-parallel']; - if ($fixerSuggestionId !== null) { - $options[] = '--restore-result-cache'; - $options[] = $fixerSuggestionId; - } $process = new ProcessPromise($loop, 'changedFileAnalysis', ProcessHelper::getWorkerCommand( $mainScript, 'fixer:worker', @@ -556,17 +475,7 @@ private function reanalyseAfterFileChanges( private function isDockerRunning(): bool { - if (!is_file('/proc/1/cgroup')) { - return false; - } - - try { - $contents = FileReader::read('/proc/1/cgroup'); - - return strpos($contents, 'docker') !== false; - } catch (CouldNotReadFileException) { - return false; - } + return is_file('/.dockerenv'); } } diff --git a/src/Command/FixerWorkerCommand.php b/src/Command/FixerWorkerCommand.php index fa14b145e3..4cb61802c8 100644 --- a/src/Command/FixerWorkerCommand.php +++ b/src/Command/FixerWorkerCommand.php @@ -3,10 +3,21 @@ namespace PHPStan\Command; use Nette\Utils\Json; -use PHPStan\Analyser\AnalyserResult; +use PHPStan\AnalysedCodeException; +use PHPStan\Analyser\Error; use PHPStan\Analyser\IgnoredErrorHelper; -use PHPStan\Analyser\ResultCache\ResultCacheManager; 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\Reflector\Exception\IdentifierNotFound; +use PHPStan\Collectors\CollectedData; +use PHPStan\DependencyInjection\Container; +use PHPStan\File\PathNotFoundException; +use PHPStan\Node\CollectedDataNode; +use PHPStan\Rules\Registry as RuleRegistry; use PHPStan\ShouldNotHappenException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -17,6 +28,7 @@ use function is_array; use function is_bool; use function is_string; +use function sprintf; class FixerWorkerCommand extends Command { @@ -44,10 +56,7 @@ protected function configure(): void 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'), ]); } @@ -74,32 +83,9 @@ 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'); - /** @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 +99,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $level, $allowXdebug, false, - $singleReflectionFile, - $insteadOfFile, false, ); } catch (InceptionNotSuccessfulException) { @@ -123,25 +107,24 @@ protected function execute(InputInterface $input, OutputInterface $output): int $container = $inceptionResult->getContainer(); - /** @var IgnoredErrorHelper $ignoredErrorHelper */ $ignoredErrorHelper = $container->getByType(IgnoredErrorHelper::class); $ignoredErrorHelperResult = $ignoredErrorHelper->initialize(); if (count($ignoredErrorHelperResult->getErrors()) > 0) { 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); + $resultCacheManager = $container->getByType(ResultCacheManagerFactory::class)->create(); $projectConfigArray = $inceptionResult->getProjectConfigArray(); - [$inceptionFiles, $isOnlyFiles] = $inceptionResult->getFiles(); - $resultCache = $resultCacheManager->restore($inceptionFiles, false, false, $projectConfigArray, $inceptionResult->getErrorOutput(), $restoreResultCache); + + try { + [$inceptionFiles, $isOnlyFiles] = $inceptionResult->getFiles(); + } catch (PathNotFoundException | InceptionNotSuccessfulException) { + return 1; + } + + $resultCache = $resultCacheManager->restore($inceptionFiles, false, false, $projectConfigArray, $inceptionResult->getErrorOutput()); $intermediateAnalyserResult = $analyserRunner->runAnalyser( $resultCache->getFilesToAnalyse(), @@ -151,24 +134,29 @@ protected function execute(InputInterface $input, OutputInterface $output): int false, $allowParallel, $configuration, - $tmpFile, - $insteadOfFile, $input, ); $result = $resultCacheManager->process( - $this->switchTmpFileInAnalyserResult($intermediateAnalyserResult, $tmpFile, $insteadOfFile), + $intermediateAnalyserResult, $resultCache, $inceptionResult->getErrorOutput(), false, is_string($saveResultCache) ? $saveResultCache : $saveResultCache === null, )->getAnalyserResult(); + $hasInternalErrors = count($result->getInternalErrors()) > 0 || $result->hasReachedInternalErrorsCountLimit(); $intermediateErrors = $ignoredErrorHelperResult->process( $result->getErrors(), $isOnlyFiles, $inceptionFiles, - count($result->getInternalErrors()) > 0 || $result->hasReachedInternalErrorsCountLimit(), + $hasInternalErrors, ); + if (!$hasInternalErrors) { + foreach ($this->getCollectedDataErrors($container, $result->getCollectedData(), $isOnlyFiles) as $error) { + $intermediateErrors[] = $error; + } + } + $finalFileSpecificErrors = []; $finalNotFileSpecificErrors = []; foreach ($intermediateErrors as $intermediateError) { @@ -188,88 +176,40 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } - private function switchTmpFileInAnalyserResult( - AnalyserResult $analyserResult, - ?string $insteadOfFile, - ?string $tmpFile, - ): AnalyserResult + /** + * @param CollectedData[] $collectedData + * @return Error[] + */ + private function getCollectedDataErrors(Container $container, array $collectedData, bool $onlyFiles): array { - $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); - } - } - - $fileSpecificErrors[] = $error; - } - - $collectedData = []; - foreach ($analyserResult->getCollectedData() as $data) { - if ( - $tmpFile !== null - && $insteadOfFile !== null - ) { - if ($data->getFilePath() === $insteadOfFile) { - $data = $data->changeFilePath($tmpFile); - } - } - - $collectedData[] = $data; - } - - $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; - } - - $new[] = $file; - } - - $key = $dependencyFile; - if ($key === $insteadOfFile && $tmpFile !== null) { - $key = $tmpFile; - } - - $dependencies[$key] = $new; + $nodeType = CollectedDataNode::class; + $node = new CollectedDataNode($collectedData, $onlyFiles); + $file = 'N/A'; + $scope = $container->getByType(ScopeFactory::class)->create(ScopeContext::create($file)); + $ruleRegistry = $container->getByType(RuleRegistry::class); + $ruleErrorTransformer = $container->getByType(RuleErrorTransformer::class); + + $errors = []; + foreach ($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 | CircularReference $e) { + $errors[] = new Error(sprintf('Reflection error: %s', $e->getMessage()), $file, $node->getLine(), $e); + continue; } - } - $exportedNodes = []; - foreach ($analyserResult->getExportedNodes() as $file => $fileExportedNodes) { - if ( - $tmpFile !== null - && $insteadOfFile !== null - && $file === $insteadOfFile - ) { - $file = $tmpFile; + foreach ($ruleErrors as $ruleError) { + $errors[] = $ruleErrorTransformer->transform($ruleError, $scope, $nodeType, $node->getLine()); } - - $exportedNodes[$file] = $fileExportedNodes; } - return new AnalyserResult( - $fileSpecificErrors, - $analyserResult->getInternalErrors(), - $collectedData, - $dependencies, - $exportedNodes, - $analyserResult->hasReachedInternalErrorsCountLimit(), - $analyserResult->getPeakMemoryUsageBytes(), - ); + return $errors; } } 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 5e27b036aa..308935fb9d 100644 --- a/src/Command/InceptionResult.php +++ b/src/Command/InceptionResult.php @@ -3,7 +3,10 @@ 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; class InceptionResult @@ -31,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(); } @@ -81,7 +87,10 @@ public function getGenerateBaselineFile(): ?string public function handleReturn(int $exitCode, ?int $peakMemoryUsageBytes): int { if ($peakMemoryUsageBytes !== null && $this->getErrorOutput()->isVerbose()) { - $this->getErrorOutput()->writeLineFormatted(sprintf('Used memory: %s', BytesHelper::bytes($peakMemoryUsageBytes))); + $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 0a98d508af..e1d0076f83 100644 --- a/src/Command/WorkerCommand.php +++ b/src/Command/WorkerCommand.php @@ -23,8 +23,6 @@ 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 defined; use function is_array; @@ -63,8 +61,6 @@ protected function configure(): void 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), ]); } @@ -92,17 +88,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, @@ -116,8 +101,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $level, $allowXdebug, false, - $singleReflectionFile, - null, false, ); } catch (InceptionNotSuccessfulException $e) { @@ -129,27 +112,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 { + $tcpConector->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(); @@ -170,8 +153,6 @@ private function runWorker( ReadableStreamInterface $in, OutputInterface $output, array $analysedFiles, - ?string $tmpFile, - ?string $insteadOfFile, ): void { $handleError = function (Throwable $error) use ($out, $output): void { @@ -189,13 +170,10 @@ private function runWorker( $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; @@ -209,9 +187,6 @@ private function runWorker( $exportedNodes = []; foreach ($files as $file) { try { - if ($file === $insteadOfFile) { - $file = $tmpFile; - } $fileAnalyserResult = $fileAnalyser->analyseFile($file, $analysedFiles, $ruleRegistry, $collectorRegistry, null); $fileErrors = $fileAnalyserResult->getErrors(); $dependencies[$file] = $fileAnalyserResult->getDependencies(); @@ -225,11 +200,13 @@ private function runWorker( } 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); } @@ -253,27 +230,4 @@ private function runWorker( $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 e331eb2222..5c4af0aff1 100644 --- a/src/Dependency/DependencyResolver.php +++ b/src/Dependency/DependencyResolver.php @@ -88,8 +88,8 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies } } 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); } @@ -115,18 +115,19 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies } } } elseif ($node instanceof Closure || $node instanceof Node\Expr\ArrowFunction) { - /** @var ClosureType $closureType */ $closureType = $scope->getType($node); - foreach ($closureType->getParameters() as $parameter) { - $referencedClasses = $parameter->getType()->getReferencedClasses(); - foreach ($referencedClasses as $referencedClass) { - $this->addClassToDependencies($referencedClass, $dependenciesReflections); + 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; @@ -365,6 +366,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); @@ -403,12 +420,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 ( @@ -475,6 +493,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)) { @@ -485,7 +512,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; } @@ -537,6 +577,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); } 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 cbc018da2a..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,12 @@ 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; @@ -31,14 +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_hash; +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; @@ -52,7 +65,7 @@ class ContainerFactory private string $configDirectory; - private static ?string $lastInitializedContainerId = null; + private static ?int $lastInitializedContainerId = null; /** @api */ public function __construct(private string $currentWorkingDirectory, private bool $checkDuplicateFiles = false) @@ -86,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(), ], ); @@ -116,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) { @@ -136,6 +147,7 @@ public function create( $configurator->setAllConfigFiles($allConfigFiles); $container = $configurator->createContainer()->getByType(Container::class); + $this->validateParameters($container->getParameters(), $projectConfig['parametersSchema']); self::postInitializeContainer($container); return $container; @@ -144,7 +156,7 @@ public function create( /** @internal */ public static function postInitializeContainer(Container $container): void { - $containerId = spl_object_hash($container); + $containerId = spl_object_id($container); if ($containerId === self::$lastInitializedContainerId) { return; } @@ -166,12 +178,13 @@ public static function postInitializeContainer(Container $container): void $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'); @@ -233,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( @@ -245,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)); @@ -267,7 +285,7 @@ private function detectDuplicateIncludedFiles( /** * @param array $loaderParameters - * @return string[] + * @return array{list, array} */ private static function getConfigFiles( FileHelper $fileHelper, @@ -279,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')) { @@ -296,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 @@ -310,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 3eb8fbd374..0000000000 --- a/src/DependencyInjection/Reflection/DirectClassReflectionExtensionRegistryProvider.php +++ /dev/null @@ -1,62 +0,0 @@ -broker = $broker; - } - - public function addPropertiesClassReflectionExtension(PropertiesClassReflectionExtension $extension): void - { - $this->propertiesClassReflectionExtensions[] = $extension; - } - - public function addMethodsClassReflectionExtension(MethodsClassReflectionExtension $extension): void - { - $this->methodsClassReflectionExtensions[] = $extension; - } - - public function addAllowedSubTypesClassReflectionExtension(AllowedSubTypesClassReflectionExtension $extension): void - { - $this->allowedSubTypesClassReflectionExtensions[] = $extension; - } - - public function getRegistry(): ClassReflectionExtensionRegistry - { - return new ClassReflectionExtensionRegistry( - $this->broker, - $this->propertiesClassReflectionExtensions, - $this->methodsClassReflectionExtensions, - $this->allowedSubTypesClassReflectionExtensions, - ); - } - -} diff --git a/src/DependencyInjection/Reflection/LazyClassReflectionExtensionRegistryProvider.php b/src/DependencyInjection/Reflection/LazyClassReflectionExtensionRegistryProvider.php index bcc28e7696..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 @@ -32,6 +34,8 @@ public function getRegistry(): ClassReflectionExtensionRegistry 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 543145d46d..9bda0aa894 100644 --- a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php +++ b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php @@ -20,6 +20,7 @@ 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; @@ -59,6 +60,7 @@ 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, diff --git a/src/File/FileExcluder.php b/src/File/FileExcluder.php index e44dedee53..2ea5271d5d 100644 --- a/src/File/FileExcluder.php +++ b/src/File/FileExcluder.php @@ -5,8 +5,8 @@ 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; @@ -65,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 68b5df087d..33f4878cb4 100644 --- a/src/File/FileHelper.php +++ b/src/File/FileHelper.php @@ -7,11 +7,12 @@ use function explode; use function implode; use function ltrim; +use function preg_match; use function rtrim; use function str_replace; use function str_starts_with; use function strlen; -use function strpos; +use function strtolower; use function substr; use function trim; use const DIRECTORY_SEPARATOR; @@ -43,7 +44,7 @@ public function absolutizePath(string $path): string return $path; } } - if (str_starts_with($path, 'phar://')) { + if (preg_match('~^[a-z0-9+\-.]+://~i', $path) === 1) { return $path; } @@ -64,11 +65,12 @@ public function normalizePath(string $originalPath, string $directorySeparator = $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; @@ -76,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 = []; 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/FileReader.php b/src/File/FileReader.php index 429976c097..46f8933916 100644 --- a/src/File/FileReader.php +++ b/src/File/FileReader.php @@ -8,6 +8,9 @@ class FileReader { + /** + * @throws CouldNotReadFileException + */ public static function read(string $fileName): string { $path = $fileName; diff --git a/src/File/FuzzyRelativePathHelper.php b/src/File/FuzzyRelativePathHelper.php index d8bb21ba88..3654c10c08 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; @@ -107,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/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 @@ +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..061c1e9e70 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->getLine(), + $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(), $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 cee3b908e5..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,11 +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()); @@ -52,6 +55,11 @@ public function isPromoted(): bool return $this->isPromoted; } + public function isPromotedFromTrait(): bool + { + return $this->isPromotedFromTrait; + } + public function getPhpDoc(): ?string { return $this->phpDoc; @@ -111,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 6b75bb9446..904186224f 100644 --- a/src/Node/ClassStatementsGatherer.php +++ b/src/Node/ClassStatementsGatherer.php @@ -17,9 +17,10 @@ use PHPStan\Node\Property\PropertyWrite; use PHPStan\Reflection\ClassReflection; use PHPStan\ShouldNotHappenException; -use PHPStan\Type\ThisType; +use PHPStan\Type\TypeUtils; use function count; use function in_array; +use function strtolower; class ClassStatementsGatherer { @@ -50,6 +51,9 @@ class ClassStatementsGatherer /** @var ClassConstantFetch[] */ private array $constantFetches = []; + /** @var array */ + private array $returnStatementNodes = []; + /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ @@ -109,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; @@ -130,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; @@ -150,6 +163,10 @@ 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 @@ -167,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) { @@ -184,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) { @@ -219,7 +236,7 @@ private function tryToApplyPropertyReads(Expr\FuncCall $node, Scope $scope): voi } $firstArgValue = $args[0]->value; - if (!$scope->getType($firstArgValue) instanceof ThisType) { + if (TypeUtils::findThisType($scope->getType($firstArgValue)) === null) { return; } diff --git a/src/Node/ClosureReturnStatementsNode.php b/src/Node/ClosureReturnStatementsNode.php index 74dbfee335..07eb1b9f95 100644 --- a/src/Node/ClosureReturnStatementsNode.php +++ b/src/Node/ClosureReturnStatementsNode.php @@ -8,6 +8,7 @@ use PhpParser\Node\Expr\YieldFrom; use PhpParser\NodeAbstract; use PHPStan\Analyser\StatementResult; +use function count; /** @api */ class ClosureReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode @@ -16,14 +17,16 @@ 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 */ public function __construct( Closure $closureExpr, private array $returnStatements, private array $yieldStatements, private StatementResult $statementResult, + private array $executionEnds, ) { parent::__construct($closureExpr->getAttributes()); @@ -40,22 +43,26 @@ public function hasNativeReturnTypehint(): bool return $this->closureExpr->returnType !== null; } - /** - * @return ReturnStatement[] - */ public function getReturnStatements(): array { return $this->returnStatements; } - /** - * @return array - */ + public function getExecutionEnds(): array + { + return $this->executionEnds; + } + 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..0b8f4b4494 100644 --- a/src/Node/FunctionReturnStatementsNode.php +++ b/src/Node/FunctionReturnStatementsNode.php @@ -2,31 +2,35 @@ namespace PHPStan\Node; +use PhpParser\Node\Expr\Yield_; +use PhpParser\Node\Expr\YieldFrom; use PhpParser\Node\Stmt\Function_; use PhpParser\NodeAbstract; 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 */ public function __construct( private Function_ $function, private array $returnStatements, + private array $yieldStatements, private StatementResult $statementResult, private array $executionEnds, + private FunctionReflection $functionReflection, ) { parent::__construct($function->getAttributes()); } - /** - * @return ReturnStatement[] - */ public function getReturnStatements(): array { return $this->returnStatements; @@ -37,9 +41,6 @@ public function getStatementResult(): StatementResult return $this->statementResult; } - /** - * @return ExecutionEndNode[] - */ public function getExecutionEnds(): array { return $this->executionEnds; @@ -55,6 +56,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 +79,9 @@ public function getSubNodeNames(): array return []; } + public function getFunctionReflection(): FunctionReflection + { + 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/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..edf8e216bc 100644 --- a/src/Node/MethodReturnStatementsNode.php +++ b/src/Node/MethodReturnStatementsNode.php @@ -2,9 +2,14 @@ namespace PHPStan\Node; +use PhpParser\Node\Expr\Yield_; +use PhpParser\Node\Expr\YieldFrom; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\NodeAbstract; use PHPStan\Analyser\StatementResult; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ExtendedMethodReflection; +use function count; /** @api */ class MethodReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode @@ -13,23 +18,24 @@ class MethodReturnStatementsNode extends NodeAbstract implements ReturnStatement private ClassMethod $classMethod; /** - * @param ReturnStatement[] $returnStatements - * @param ExecutionEndNode[] $executionEnds + * @param list $returnStatements + * @param list $yieldStatements + * @param list $executionEnds */ public function __construct( ClassMethod $method, private array $returnStatements, + private array $yieldStatements, private StatementResult $statementResult, private array $executionEnds, + private ClassReflection $classReflection, + private ExtendedMethodReflection $methodReflection, ) { parent::__construct($method->getAttributes()); $this->classMethod = $method; } - /** - * @return ReturnStatement[] - */ public function getReturnStatements(): array { return $this->returnStatements; @@ -40,9 +46,6 @@ public function getStatementResult(): StatementResult return $this->statementResult; } - /** - * @return ExecutionEndNode[] - */ public function getExecutionEnds(): array { return $this->executionEnds; @@ -58,6 +61,31 @@ 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; + } + + public function isGenerator(): bool + { + return count($this->yieldStatements) > 0; + } + public function getType(): string { return 'PHPStan_Node_MethodReturnStatementsNode'; 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 34357cbcc5..8ba7687219 100644 --- a/src/Node/ReturnStatementsNode.php +++ b/src/Node/ReturnStatementsNode.php @@ -2,6 +2,8 @@ namespace PHPStan\Node; +use PhpParser\Node\Expr\Yield_; +use PhpParser\Node\Expr\YieldFrom; use PHPStan\Analyser\StatementResult; /** @api */ @@ -9,14 +11,26 @@ interface ReturnStatementsNode extends VirtualNode { /** - * @return ReturnStatement[] + * @return list */ public function getReturnStatements(): array; public function getStatementResult(): StatementResult; + /** + * @return list + */ + public function getExecutionEnds(): array; + public function returnsByRef(): bool; public function hasNativeReturnTypehint(): bool; + /** + * @return list + */ + public function getYieldStatements(): array; + + public function isGenerator(): bool; + } 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/Parallel/ParallelAnalyser.php b/src/Parallel/ParallelAnalyser.php index 501d1683fc..3ea820abff 100644 --- a/src/Parallel/ParallelAnalyser.php +++ b/src/Parallel/ParallelAnalyser.php @@ -22,12 +22,13 @@ 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 @@ -56,8 +57,6 @@ public function analyse( string $mainScript, ?Closure $postFileCallback, ?string $projectConfigFile, - ?string $tmpFile, - ?string $insteadOfFile, InputInterface $input, ): AnalyserResult { @@ -128,13 +127,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', @@ -214,7 +206,26 @@ 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); 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/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/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 index 166b923990..5edd559ed0 100644 --- a/src/Parser/LastConditionVisitor.php +++ b/src/Parser/LastConditionVisitor.php @@ -10,14 +10,18 @@ class LastConditionVisitor extends NodeVisitorAbstract { public const ATTRIBUTE_NAME = 'isLastCondition'; + public const ATTRIBUTE_IS_MATCH_NAME = 'isMatch'; public function enterNode(Node $node): ?Node { if ($node instanceof Node\Stmt\If_ && $node->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 && $node->else === null; + $isLast = $i === $lastElseIf && $elseIsMissingOrThrowing; $elseif->cond->setAttribute(self::ATTRIBUTE_NAME, $isLast); } } @@ -31,8 +35,46 @@ public function enterNode(Node $node): ?Node } $isLast = $i === $lastArm; - $arm->conds[0]->setAttribute(self::ATTRIBUTE_NAME, $isLast); + $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/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..da9b8b83a0 100644 --- a/src/Parser/RichParser.php +++ b/src/Parser/RichParser.php @@ -10,9 +10,18 @@ use PHPStan\DependencyInjection\Container; use PHPStan\File\FileReader; use PHPStan\ShouldNotHappenException; +use function array_filter; +use function count; +use function implode; use function is_string; +use function preg_match_all; +use function sprintf; +use function str_contains; 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; @@ -21,11 +30,16 @@ 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 bool $enableIgnoreErrorsWithinPhpDocs = false, ) { } @@ -61,14 +75,22 @@ 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); + $linesToIgnore = $this->getLinesToIgnore($tokens); if (isset($nodes[0])) { - $nodes[0]->setAttribute('linesToIgnore', $this->getLinesToIgnore($tokens)); + $nodes[0]->setAttribute('linesToIgnore', $linesToIgnore); + } + + 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; @@ -76,7 +98,7 @@ public function parseString(string $sourceCode): array /** * @param mixed[] $tokens - * @return int[] + * @return array|null> */ private function getLinesToIgnore(array $tokens): array { @@ -93,15 +115,65 @@ private function getLinesToIgnore(array $tokens): array $text = $token[1]; $line = $token[2]; - if (strpos($text, '@phpstan-ignore-next-line') !== false) { - $line++; - } elseif (strpos($text, '@phpstan-ignore-line') === false) { - continue; + + if ($this->enableIgnoreErrorsWithinPhpDocs && $type === T_DOC_COMMENT) { + $lines += $this->getLinesToIgnoreForTokenByIgnoreComment($text, $line, '@phpstan-ignore-line'); + if (str_contains($text, '@phpstan-ignore-next-line')) { + $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); + } + } else { + if (str_contains($text, '@phpstan-ignore-next-line')) { + $line++; + } elseif (!str_contains($text, '@phpstan-ignore-line')) { + continue; + } + + $line += substr_count($token[1], "\n"); + $lines[$line] = null; } + } - $line += substr_count($token[1], "\n"); + return $lines; + } + + /** + * @return array + */ + private function getLinesToIgnoreForTokenByIgnoreComment( + string $tokenText, + int $tokenLine, + string $ignoreComment, + bool $ignoreNextLine = false, + ): array + { + $lines = []; + $positionsOfIgnoreComment = []; + $offset = 0; + + while (($pos = strpos($tokenText, $ignoreComment, $offset)) !== false) { + $positionsOfIgnoreComment[] = $pos; + $offset = $pos + 1; + } - $lines[] = $line; + foreach ($positionsOfIgnoreComment as $pos) { + $line = $tokenLine + substr_count(substr($tokenText, 0, $pos), "\n") + ($ignoreNextLine ? 1 : 0); + $lines[$line] = null; } return $lines; 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/Php/PhpVersion.php b/src/Php/PhpVersion.php index f6afda248a..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; @@ -216,4 +231,60 @@ 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 index 4ed389c2a9..aa2ca2657d 100644 --- a/src/PhpDoc/ConstExprParserFactory.php +++ b/src/PhpDoc/ConstExprParserFactory.php @@ -13,7 +13,7 @@ public function __construct(private bool $unescapeStrings) public function create(): ConstExprParser { - return new ConstExprParser($this->unescapeStrings); + return new ConstExprParser($this->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/JsonValidateStubFilesExtension.php b/src/PhpDoc/JsonValidateStubFilesExtension.php new file mode 100644 index 0000000000..19c1f824d0 --- /dev/null +++ b/src/PhpDoc/JsonValidateStubFilesExtension.php @@ -0,0 +1,23 @@ +phpVersion->supportsJsonValidate()) { + return []; + } + + return [__DIR__ . '/../../stubs/json_validate.stub']; + } + +} 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 bb16d18b31..8b3745e590 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -14,6 +14,8 @@ 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; @@ -28,17 +30,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 method_exists; -use function strpos; +use function str_starts_with; use function substr; class PhpDocNodeResolver @@ -103,8 +109,8 @@ public function resolvePropertyTags(PhpDocNode $phpDocNode, NameScope $nameScope $resolved[$propertyName] = new PropertyTag( $propertyType, - true, - true, + $propertyType, + $propertyType, ); } } @@ -114,10 +120,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, ); } } @@ -127,10 +138,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, ); } } @@ -144,9 +160,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 ($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); @@ -178,6 +214,7 @@ public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope): : new MixedType(), $tagValue->isStatic, $parameters, + $templateTags, ); } } @@ -270,9 +307,9 @@ public function resolveTemplateTags(PhpDocNode $phpDocNode, NameScope $nameScope continue; } - if (strpos($tagName, '@psalm-') === 0) { + if (str_starts_with($tagName, '@psalm-')) { $prefix = 'psalm'; - } elseif (strpos($tagName, '@phpstan-') === 0) { + } elseif (str_starts_with($tagName, '@phpstan-')) { $prefix = 'phpstan'; } else { $prefix = ''; @@ -398,6 +435,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 */ @@ -466,19 +539,19 @@ private function resolveAssertTagsFor(PhpDocNode $phpDocNode, NameScope $nameSco 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); + $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); + $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); + $resolved[] = new AssertTag($if, $type, $parameter, $assertTagValue->isNegated, $assertTagValue->isEquality ?? false, true); } return $resolved; @@ -517,6 +590,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'); @@ -599,7 +679,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/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 c3215d3058..62d6cbd958 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -12,6 +12,8 @@ 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; @@ -22,9 +24,13 @@ 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; @@ -57,6 +63,8 @@ class ResolvedPhpDocBlock private PhpDocNodeResolver $phpDocNodeResolver; + private ReflectionProvider $reflectionProvider; + /** @var array<(string|int), VarTag>|false */ private array|false $varTags = false; @@ -88,6 +96,12 @@ 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; @@ -103,6 +117,8 @@ class ResolvedPhpDocBlock private ?bool $isDeprecated = null; + private ?bool $isNotDeprecated = null; + private ?bool $isInternal = null; private ?bool $isFinal = null; @@ -135,6 +151,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() @@ -147,6 +164,7 @@ public static function create( $self->templateTypeMap = $templateTypeMap; $self->templateTags = $templateTags; $self->phpDocNodeResolver = $phpDocNodeResolver; + $self->reflectionProvider = $reflectionProvider; return $self; } @@ -171,12 +189,15 @@ public static function createEmpty(): self $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; @@ -195,6 +216,11 @@ 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 @@ -222,15 +248,18 @@ public function merge(array $parents, array $parentPhpDocBlocks): self $result->usesTags = $this->getUsesTags(); $result->paramTags = self::mergeParamTags($this->getParamTags(), $parents, $parentPhpDocBlocks); $result->paramOutTags = self::mergeParamOutTags($this->getParamOutTags(), $parents, $parentPhpDocBlocks); - $result->returnTag = self::mergeReturnTags($this->getReturnTag(), $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->assertTags = self::mergeAssertTags($this->getAssertTags(), $parents, $parentPhpDocBlocks); $result->selfOutTypeTag = self::mergeSelfOutTypeTags($this->getSelfOutTag(), $parents); - $result->deprecatedTag = self::mergeDeprecatedTags($this->getDeprecatedTag(), $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(); @@ -252,6 +281,17 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self return $this; } + $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); + }; + $paramTags = $this->getParamTags(); $newParamTags = []; @@ -259,7 +299,8 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self if (!array_key_exists($key, $parameterNameMapping)) { continue; } - $newParamTags[$parameterNameMapping[$key]] = $paramTag; + $transformedType = TypeTraverser::map($paramTag->getType(), $mapParameterCb); + $newParamTags[$parameterNameMapping[$key]] = $paramTag->withType($transformedType); } $paramOutTags = $this->getParamOutTags(); @@ -269,21 +310,14 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self if (!array_key_exists($key, $parameterNameMapping)) { continue; } - $newParamOutTags[$parameterNameMapping[$key]] = $paramOutTag; + + $transformedType = TypeTraverser::map($paramOutTag->getType(), $mapParameterCb); + $newParamOutTags[$parameterNameMapping[$key]] = $paramOutTag->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); } @@ -307,6 +341,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; @@ -318,12 +353,15 @@ public function changeParameterNamesByMapping(array $parameterNameMapping): self $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; @@ -521,6 +559,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 */ @@ -599,6 +667,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) { @@ -653,14 +734,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; @@ -732,7 +813,7 @@ private static function mergeVarTags(array $varTags, array $parents, array $pare 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; @@ -767,7 +848,7 @@ private static function mergeOneParentParamTags(array $paramTags, self $parent, continue; } - $paramTags[$name] = self::resolveTemplateTypeInTag($parentParamTag, $phpDocBlock); + $paramTags[$name] = self::resolveTemplateTypeInTag($parentParamTag, $phpDocBlock, TemplateTypeVariance::createContravariant()); } return $paramTags; @@ -778,14 +859,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; } @@ -796,7 +877,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) { @@ -805,6 +886,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()) { @@ -816,6 +912,7 @@ private static function mergeOneParentReturnTag(?ReturnTag $returnTag, self $par $phpDocBlock->transformConditionalReturnTypeWithParameterNameMapping($parentReturnTag->getType()), )->toImplicit(), $phpDocBlock, + TemplateTypeVariance::createCovariant(), ); } @@ -839,8 +936,12 @@ private static function mergeAssertTags(array $assertTags, array $parents, array $phpDocBlock = $parentPhpDocBlocks[$i]; return array_map( - static fn (AssertTag $assertTag) => $assertTag->withParameter( - $phpDocBlock->transformAssertTagParameterWithParameterNameMapping($assertTag->getParameter()), + static fn (AssertTag $assertTag) => self::resolveTemplateTypeInTag( + $assertTag->withParameter( + $phpDocBlock->transformAssertTagParameterWithParameterNameMapping($assertTag->getParameter()), + )->toImplicit(), + $phpDocBlock, + TemplateTypeVariance::createCovariant(), ), $result, ); @@ -871,14 +972,19 @@ private static function mergeSelfOutTypeTags(?SelfOutTypeTag $selfOutTypeTag, ar /** * @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; @@ -936,7 +1042,7 @@ private static function mergeOneParentParamOutTags(array $paramOutTags, self $pa continue; } - $paramOutTags[$name] = self::resolveTemplateTypeInTag($parentParamTag, $phpDocBlock); + $paramOutTags[$name] = self::resolveTemplateTypeInTag($parentParamTag, $phpDocBlock, TemplateTypeVariance::createCovariant()); } return $paramOutTags; @@ -947,11 +1053,17 @@ private static function mergeOneParentParamOutTags(array $paramOutTags, self $pa * @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/StubValidator.php b/src/PhpDoc/StubValidator.php index 578b719bb0..f707c85895 100644 --- a/src/PhpDoc/StubValidator.php +++ b/src/PhpDoc/StubValidator.php @@ -12,15 +12,20 @@ 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; @@ -47,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; @@ -84,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', ]); @@ -91,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); @@ -129,6 +135,7 @@ static function (): void { Broker::registerInstance($originalBroker); ReflectionProviderStaticAccessor::registerInstance($originalReflectionProvider); + PhpVersionStaticAccessor::registerInstance($originalPhpVerison); ObjectType::resetCaches(); return $errors; @@ -142,24 +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), @@ -171,15 +183,13 @@ 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), @@ -198,6 +208,14 @@ private function getRuleRegistry(Container $container): RuleRegistry $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 index 4e436873a2..301b5b4876 100644 --- a/src/PhpDoc/Tag/AssertTag.php +++ b/src/PhpDoc/Tag/AssertTag.php @@ -18,7 +18,7 @@ final class AssertTag implements TypedTag /** * @param self::NULL|self::IF_TRUE|self::IF_FALSE $if */ - public function __construct(private string $if, private Type $type, private AssertTagParameter $parameter, private bool $negated, private bool $equality) + public function __construct(private string $if, private Type $type, private AssertTagParameter $parameter, private bool $negated, private bool $equality, private bool $isExplicit) { } @@ -60,14 +60,14 @@ public function isEquality(): bool */ public function withType(Type $type): TypedTag { - $tag = new self($this->if, $type, $this->parameter, $this->negated, $this->equality); + $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); + $tag = new self($this->if, $this->type, $parameter, $this->negated, $this->equality, $this->isExplicit); $tag->originalType = $this->getOriginalType(); return $tag; } @@ -78,9 +78,19 @@ public function negate(): self throw new ShouldNotHappenException(); } - $tag = new self($this->if, $this->type, $this->parameter, !$this->negated, $this->equality); + $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/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/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/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 6388202c76..675eb8a626 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,7 +29,9 @@ 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; @@ -63,21 +66,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; @@ -99,14 +110,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, @@ -159,10 +174,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(); @@ -187,6 +206,12 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco 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(); @@ -197,6 +222,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': @@ -214,6 +240,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); @@ -259,6 +297,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(); @@ -306,6 +351,7 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco return new IterableType(new MixedType(), new MixedType()); case 'callable': + case 'pure-callable': return new CallableType(); case 'resource': @@ -317,9 +363,16 @@ 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(); @@ -329,6 +382,9 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco 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); @@ -337,12 +393,12 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco return $type; } - return new NeverType(true); + return new NonAcceptingNeverType(); case 'never-return': case 'never-returns': case 'no-return': - return new NeverType(true); + return new NonAcceptingNeverType(); case 'list': return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new MixedType())); @@ -351,6 +407,11 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco 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); @@ -359,6 +420,8 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco } return StaticTypeFactory::falsey(); + case '__stringandstringable': + return new StringAlwaysAcceptingObjectWithToStringType(); } if ($nameScope->getClassName() !== null) { @@ -399,7 +462,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(); } @@ -550,8 +613,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 @@ -569,7 +647,7 @@ 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 = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $genericTypes[0])); if ($mainTypeName === 'non-empty-list') { @@ -628,32 +706,8 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na return new ErrorType(); } elseif ($mainTypeName === 'value-of') { if (count($genericTypes) === 1) { // value-of - $genericTypeObjectClassNames = $genericTypes[0]->getObjectClassNames(); - if (count($genericTypeObjectClassNames) > 1) { - throw new ShouldNotHappenException(); - } - - if ($genericTypeObjectClassNames !== []) { - if ($this->getReflectionProvider()->hasClass($genericTypeObjectClassNames[0])) { - $classReflection = $this->getReflectionProvider()->getClass($genericTypeObjectClassNames[0]); - - 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; } @@ -680,19 +734,36 @@ 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 ($mainTypeClassName !== null) { if (!$this->getReflectionProvider()->hasClass($mainTypeClassName)) { - return new GenericObjectType($mainTypeClassName, $genericTypes); + return new GenericObjectType($mainTypeClassName, $genericTypes, null, null, $variances); } $classReflection = $this->getReflectionProvider()->getClass($mainTypeClassName); @@ -706,6 +777,9 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na return new GenericObjectType($mainTypeClassName, [ new MixedType(true), $genericTypes[0], + ], null, null, [ + TemplateTypeVariance::createInvariant(), + $variances[0], ]); } @@ -713,6 +787,9 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na return new GenericObjectType($mainTypeClassName, [ $genericTypes[0], $genericTypes[1], + ], null, null, [ + $variances[0], + $variances[1], ]); } } @@ -724,6 +801,11 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na $genericTypes[0], $mixed, $mixed, + ], null, null, [ + TemplateTypeVariance::createInvariant(), + $variances[0], + TemplateTypeVariance::createInvariant(), + TemplateTypeVariance::createInvariant(), ]); } @@ -734,41 +816,60 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na $genericTypes[1], $mixed, $mixed, + ], null, null, [ + $variances[0], + $variances[1], + TemplateTypeVariance::createInvariant(), + TemplateTypeVariance::createInvariant(), ]); } } if (!$mainType->isIterable()->yes()) { - return new GenericObjectType($mainTypeClassName, $genericTypes); + return new GenericObjectType($mainTypeClassName, $genericTypes, null, null, $variances); } if ( count($genericTypes) !== 1 || $classReflection->getTemplateTypeMap()->count() === 1 ) { - return new GenericObjectType($mainTypeClassName, $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 ($mainTypeClassName !== null) { - return new GenericObjectType($mainTypeClassName, $genericTypes); + return new GenericObjectType($mainTypeClassName, $genericTypes, null, null, $variances); } return new ErrorType(); @@ -776,15 +877,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, @@ -796,16 +923,17 @@ function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadi }, $typeNode->parameters, ); + $returnType = $this->resolve($typeNode->returnType, $nameScope); if ($mainType instanceof CallableType) { - return new CallableType($parameters, $returnType, $isVariadic); + return new CallableType($parameters, $returnType, $isVariadic, $templateTypeMap, null, $templateTags); } elseif ( $mainType instanceof ObjectType && $mainType->getClassName() === Closure::class ) { - return new ClosureType($parameters, $returnType, $isVariadic); + return new ClosureType($parameters, $returnType, $isVariadic, $templateTypeMap, null, null, $templateTags); } return new ErrorType(); @@ -832,7 +960,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 @@ -929,9 +1083,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/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 @@ -variants === null) { $this->variants = [ new FunctionVariantWithPhpDocs( - TemplateTypeMap::createEmpty(), + $this->templateTypeMap, null, $this->parameters, $this->isVariadic, @@ -81,6 +83,11 @@ public function getVariants(): array return $this->variants; } + public function getNamedArgumentsVariants(): ?array + { + return null; + } + public function isDeprecated(): TrinaryLogic { return TrinaryLogic::createNo(); @@ -96,6 +103,11 @@ public function isFinal(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isInternal(): TrinaryLogic { return TrinaryLogic::createNo(); @@ -112,6 +124,10 @@ public function hasSideEffects(): TrinaryLogic return TrinaryLogic::createYes(); } + if ((new ThisType($this->declaringClass))->isSuperTypeOf($this->returnType)->yes()) { + return TrinaryLogic::createYes(); + } + return TrinaryLogic::createMaybe(); } @@ -135,4 +151,18 @@ 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/AnnotationsMethodsClassReflectionExtension.php b/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php index fff338b30f..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,6 +63,13 @@ 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'; @@ -65,6 +79,8 @@ private function findClassReflectionWithMethod( TemplateTypeHelper::resolveTemplateTypes( $methodTags[$methodName]->getReturnType(), $classReflection->getActiveTemplateTypeMap(), + $classReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createCovariant(), ), $parameters, $isStatic, @@ -72,6 +88,7 @@ private function findClassReflectionWithMethod( $classReflection->hasNativeMethod($nativeCallMethodName) ? $classReflection->getNativeMethod($nativeCallMethodName)->getThrowType() : null, + $templateTypeMap, ); } @@ -84,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/BetterReflection/BetterReflectionProvider.php b/src/Reflection/BetterReflection/BetterReflectionProvider.php index 0766abd21a..5f3ce5fd18 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; @@ -43,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; @@ -69,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, @@ -84,6 +88,8 @@ public function __construct( private AnonymousClassNameHelper $anonymousClassNameHelper, private FileHelper $fileHelper, private PhpStormStubsSourceStubber $phpstormStubsSourceStubber, + private SignatureMapProvider $signatureMapProvider, + private array $universalObjectCratesClasses, ) { } @@ -135,14 +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; @@ -211,14 +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(), $this->classReflectionExtensionRegistryProvider->getRegistry()->getAllowedSubTypesClassReflectionExtensions(), + $this->classReflectionExtensionRegistryProvider->getRegistry()->getRequireExtendsPropertyClassReflectionExtension(), + $this->classReflectionExtensionRegistryProvider->getRegistry()->getRequireExtendsMethodsClassReflectionExtension(), sprintf('class@anonymous/%s:%s', $filename, $classNode->getLine()), new ReflectionClass($reflectionClass), $scopeFile, null, $this->stubPhpDocProvider->findClassPhpDoc($className), + $this->universalObjectCratesClasses, ); $this->classReflections[$className] = self::$anonymousClasses[$className]; @@ -348,11 +362,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, ); } @@ -364,7 +406,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 fd022aaff8..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]; } diff --git a/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php b/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php index 5f205e1682..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) { diff --git a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php index 34c6105a58..3d123ccfe2 100644 --- a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php +++ b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php @@ -12,7 +12,6 @@ use PHPStan\File\FileReader; use PHPStan\Internal\ComposerHelper; use PHPStan\Php\PhpVersion; -use function array_filter; use function array_key_exists; use function array_map; use function array_merge; @@ -23,7 +22,7 @@ use function glob; use function is_dir; use function is_file; -use function strpos; +use function str_contains; use const GLOB_ONLYDIR; class ComposerJsonAndInstalledJsonSourceLocatorMaker @@ -73,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 . '/') : [], @@ -111,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; } @@ -140,17 +139,17 @@ public function create(string $projectInstallationPath): ?SourceLocator if ($phpunitBridgeDirectories !== false) { foreach (array_reverse($phpunitBridgeDirectories) as $dir) { $bestDirFound = $dir; - if ($this->phpVersion->getVersionId() >= 80100 && strpos($dir, 'phpunit-10') !== false) { + if ($this->phpVersion->getVersionId() >= 80100 && str_contains($dir, 'phpunit-10')) { break; } if ($this->phpVersion->getVersionId() >= 80000) { - if (strpos($dir, 'phpunit-9') !== false) { + if (str_contains($dir, 'phpunit-9')) { break; } continue; } - if (strpos($dir, 'phpunit-8') !== false || strpos($dir, 'phpunit-7') !== false) { + if (str_contains($dir, 'phpunit-8') || str_contains($dir, 'phpunit-7')) { break; } } 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 6c0c211e67..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; } 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/PhpFileCleaner.php b/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php index d8c2d8a789..278aa19b04 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; @@ -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/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/ClassReflection.php b/src/Reflection/ClassReflection.php index 098b2101bc..0c8b308934 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; @@ -123,6 +136,7 @@ class ClassReflection * @param PropertiesClassReflectionExtension[] $propertiesClassReflectionExtensions * @param MethodsClassReflectionExtension[] $methodsClassReflectionExtensions * @param AllowedSubTypesClassReflectionExtension[] $allowedSubTypesClassReflectionExtensions + * @param string[] $universalObjectCratesClasses */ public function __construct( private ReflectionProvider $reflectionProvider, @@ -131,15 +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, ) { } @@ -199,7 +218,8 @@ public function getParentClass(): ?ClassReflection $extendedType = TemplateTypeHelper::resolveTemplateTypes( $extendedType, $this->getPossiblyIncompleteActiveTemplateTypeMap(), - true, + $this->getCallSiteVarianceMap(), + TemplateTypeVariance::createStatic(), ); } @@ -232,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; + } + + $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 $name . '<' . implode(',', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::typeOnly()), $this->getActiveTemplateTypeMap()->getTypes())) . '>'; + return $this->displayName . '<' . implode(',', $templateTypes) . '>'; } public function getCacheKey(): string @@ -255,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) { @@ -358,7 +398,11 @@ public function allowsDynamicProperties(): bool return false; } - if ($this->is(stdClass::class)) { + if (UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate( + $this->reflectionProvider, + $this->universalObjectCratesClasses, + $this, + )) { return true; } @@ -378,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 @@ -389,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; } @@ -407,6 +478,10 @@ public function hasMethod(string $methodName): bool } } + if ($this->requireExtendsMethodsClassReflectionExtension->hasMethod($this, $methodName)) { + return true; + } + return false; } @@ -416,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)) { @@ -430,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); } @@ -538,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; } @@ -555,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); } @@ -591,9 +683,12 @@ 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(); } public function isReadOnly(): bool @@ -620,12 +715,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 @@ -634,10 +724,6 @@ public function hasEnumCase(string $name): bool return false; } - if (!$this->reflection instanceof ReflectionEnum) { - return false; - } - return $this->reflection->hasCase($name); } @@ -646,15 +732,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(); @@ -831,6 +918,8 @@ public function getImmediateInterfaces(): array $implementedType = TemplateTypeHelper::resolveTemplateTypes( $implementedType, $this->getPossiblyIncompleteActiveTemplateTypeMap(), + $this->getCallSiteVarianceMap(), + TemplateTypeVariance::createStatic(), true, ); } @@ -889,15 +978,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; @@ -917,7 +1006,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); @@ -953,10 +1042,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, @@ -977,7 +1074,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(); @@ -1240,6 +1337,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) { @@ -1266,13 +1389,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 { @@ -1289,6 +1432,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 */ @@ -1301,14 +1460,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), ); } @@ -1460,6 +1654,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 */ @@ -1501,6 +1721,8 @@ public function getResolvedMixinTypes(): array $types[] = TemplateTypeHelper::resolveTemplateTypes( $mixinTag->getType(), $this->getActiveTemplateTypeMap(), + $this->getCallSiteVarianceMap(), + TemplateTypeVariance::createStatic(), ); } diff --git a/src/Reflection/ClassReflectionExtensionRegistry.php b/src/Reflection/ClassReflectionExtensionRegistry.php index e5ed2e5425..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 @@ -18,6 +20,8 @@ public function __construct( private array $propertiesClassReflectionExtensions, private array $methodsClassReflectionExtensions, private array $allowedSubTypesClassReflectionExtensions, + private RequireExtendsPropertiesClassReflectionExtension $requireExtendsPropertiesClassReflectionExtension, + private RequireExtendsMethodsClassReflectionExtension $requireExtendsMethodsClassReflectionExtension, ) { foreach (array_merge($propertiesClassReflectionExtensions, $methodsClassReflectionExtensions, $allowedSubTypesClassReflectionExtensions) as $extension) { @@ -53,4 +57,14 @@ 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 77ecd6a4bd..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; @@ -16,6 +17,7 @@ final class ConstructorsHelper * @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 d0ee920f86..4fc7daca3d 100644 --- a/src/Reflection/Dummy/ChangedTypeMethodReflection.php +++ b/src/Reflection/Dummy/ChangedTypeMethodReflection.php @@ -9,14 +9,16 @@ use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; +use function is_bool; class ChangedTypeMethodReflection implements ExtendedMethodReflection { /** * @param ParametersAcceptorWithPhpDocs[] $variants + * @param ParametersAcceptorWithPhpDocs[]|null $namedArgumentsVariants */ - public function __construct(private ClassReflection $declaringClass, private ExtendedMethodReflection $reflection, private array $variants) + public function __construct(private ClassReflection $declaringClass, private ExtendedMethodReflection $reflection, private array $variants, private ?array $namedArgumentsVariants) { } @@ -60,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(); @@ -75,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(); @@ -105,4 +117,19 @@ 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 81c27a3b1d..806c24c815 100644 --- a/src/Reflection/Dummy/DummyMethodReflection.php +++ b/src/Reflection/Dummy/DummyMethodReflection.php @@ -58,6 +58,11 @@ public function getVariants(): array ]; } + public function getNamedArgumentsVariants(): ?array + { + return null; + } + public function isDeprecated(): TrinaryLogic { return TrinaryLogic::createMaybe(); @@ -73,6 +78,11 @@ public function isFinal(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isFinalByKeyword(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isInternal(): TrinaryLogic { return TrinaryLogic::createMaybe(); @@ -108,4 +118,14 @@ 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 8ca080639f..0ff0f8f2de 100644 --- a/src/Reflection/ExtendedMethodReflection.php +++ b/src/Reflection/ExtendedMethodReflection.php @@ -27,10 +27,28 @@ interface ExtendedMethodReflection extends MethodReflection */ 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 cd21dc5389..91afcbaadb 100644 --- a/src/Reflection/FunctionReflection.php +++ b/src/Reflection/FunctionReflection.php @@ -18,6 +18,11 @@ public function getFileName(): ?string; */ public function getVariants(): array; + /** + * @return ParametersAcceptorWithPhpDocs[]|null + */ + public function getNamedArgumentsVariants(): ?array; + public function isDeprecated(): TrinaryLogic; public function getDeprecatedDescription(): ?string; @@ -38,4 +43,13 @@ 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/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 f26654b917..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, ); } diff --git a/src/Reflection/GenericParametersAcceptorResolver.php b/src/Reflection/GenericParametersAcceptorResolver.php index 9922e0bfc7..46154a33b5 100644 --- a/src/Reflection/GenericParametersAcceptorResolver.php +++ b/src/Reflection/GenericParametersAcceptorResolver.php @@ -6,6 +6,7 @@ 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; @@ -101,12 +102,14 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc $parametersAcceptor->getReturnType(), $parametersAcceptor->getReturnType(), new MixedType(), + TemplateTypeVarianceMap::createEmpty(), ); } return new ResolvedFunctionVariant( $parametersAcceptor, $resolvedTemplateTypeMap, + $parametersAcceptor->getCallSiteVarianceMap(), $passedArgs, ); } diff --git a/src/Reflection/InaccessibleMethod.php b/src/Reflection/InaccessibleMethod.php index f089c132ad..3d732648c5 100644 --- a/src/Reflection/InaccessibleMethod.php +++ b/src/Reflection/InaccessibleMethod.php @@ -3,6 +3,7 @@ namespace PHPStan\Reflection; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\MixedType; use PHPStan\Type\Type; @@ -28,6 +29,11 @@ public function getResolvedTemplateTypeMap(): TemplateTypeMap return TemplateTypeMap::createEmpty(); } + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return TemplateTypeVarianceMap::createEmpty(); + } + /** * @return array */ diff --git a/src/Reflection/InitializerExprContext.php b/src/Reflection/InitializerExprContext.php index 5ce653ca92..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, diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index ee5a49a822..c81d6622ce 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -7,6 +7,7 @@ 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; @@ -17,6 +18,7 @@ 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; @@ -59,10 +61,12 @@ 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; @@ -71,6 +75,7 @@ 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; @@ -84,6 +89,9 @@ class InitializerExprTypeResolver public const CALCULATE_SCALARS_LIMIT = 128; + /** @var array */ + private array $currentlyResolvingClassConstant = []; + public function __construct( private ConstantResolver $constantResolver, private ReflectionProviderProvider $reflectionProviderProvider, @@ -377,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(); } @@ -484,8 +501,9 @@ public function getArrayType(Expr\Array_ $expr, callable $getTypeCallback): Type $valueType = $getTypeCallback($arrayItem->value); if ($arrayItem->unpack) { - if (count($valueType->getConstantArrays()) === 1) { - $constantArrayType = $valueType->getConstantArrays()[0]; + $constantArrays = $valueType->getConstantArrays(); + if (count($constantArrays) === 1) { + $constantArrayType = $constantArrays[0]; $hasStringKey = false; foreach ($constantArrayType->getKeyTypes() as $keyType) { if ($keyType->isString()->yes()) { @@ -542,8 +560,8 @@ public function getBitwiseAndType(Expr $left, Expr $right, callable $getTypeCall 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,8 +627,8 @@ public function getBitwiseOrType(Expr $left, Expr $right, callable $getTypeCallb 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,8 +684,8 @@ public function getBitwiseXorType(Expr $left, Expr $right, callable $getTypeCall 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) { @@ -723,22 +741,18 @@ public function getSpaceshipType(Expr $left, Expr $right, callable $getTypeCallb 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; } } @@ -756,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) { @@ -790,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(); } } @@ -816,8 +826,8 @@ public function getModType(Expr $left, Expr $right, callable $getTypeCallback): 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) { @@ -856,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; @@ -917,8 +923,8 @@ public function getPlusType(Expr $left, Expr $right, callable $getTypeCallback): 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) { @@ -1071,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) { @@ -1113,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) { @@ -1196,8 +1202,8 @@ public function getShiftLeftType(Expr $left, Expr $right, callable $getTypeCallb 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) { @@ -1253,8 +1259,8 @@ public function getShiftRightType(Expr $left, Expr $right, callable $getTypeCall 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) { @@ -1304,10 +1310,10 @@ public function resolveIdenticalType(Type $leftType, Type $rightType): BooleanTy return new ConstantBooleanType($leftType->getValue() === $rightType->getValue()); } - $leftTypeEnumCases = $leftType->getEnumCases(); - $rightTypeEnumCases = $rightType->getEnumCases(); - if (count($leftTypeEnumCases) === 1 && count($rightTypeEnumCases) === 1) { - return new ConstantBooleanType($leftTypeEnumCases[0]->equals($rightTypeEnumCases[0])); + $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); @@ -1324,37 +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 ( - (count($leftType->getEnumCases()) === 1 && count($rightType->getEnumCases()) === 1) + ($leftType->isEnum()->yes() && $rightType->isTrue()->no()) + || ($rightType->isEnum()->yes() && $leftType->isTrue()->no()) || ($leftType->isString()->yes() && $rightType->isString()->yes()) - || ($integerType->isSuperTypeOf($leftType)->yes() && $integerType->isSuperTypeOf($rightType)->yes()) - || ($floatType->isSuperTypeOf($leftType)->yes() && $floatType->isSuperTypeOf($rightType)->yes()) + || ($leftType->isInteger()->yes() && $rightType->isInteger()->yes()) + || ($leftType->isFloat()->yes() && $rightType->isFloat()->yes()) ) { return $this->resolveIdenticalType($leftType, $rightType); } - if ($leftType->isConstantArray()->yes() && $leftType->isIterableAtLeastOnce()->no() && $rightType instanceof ConstantScalarType) { - // @phpstan-ignore-next-line - return new ConstantBooleanType($rightType->getValue() == []); // phpcs:ignore - } - if ($rightType->isConstantArray()->yes() && $rightType->isIterableAtLeastOnce()->no() && $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); } /** @@ -1454,37 +1444,43 @@ private function callOperatorTypeSpecifyingExtensions(Expr\BinaryOp $expr, Type */ 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) - ) { + $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); } $specifiedTypes = $this->callOperatorTypeSpecifyingExtensions($expr, $leftType, $rightType); @@ -1492,7 +1488,6 @@ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $ri return $specifiedTypes; } - $types = TypeCombinator::union($leftType, $rightType); if ( $leftType->isArray()->yes() || $rightType->isArray()->yes() @@ -1501,8 +1496,6 @@ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $ri return new ErrorType(); } - $leftNumberType = $leftType->toNumber(); - $rightNumberType = $rightType->toNumber(); if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) { return new ErrorType(); } @@ -1511,8 +1504,8 @@ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $ri } if ( - (new FloatType())->isSuperTypeOf($leftNumberType)->yes() - || (new FloatType())->isSuperTypeOf($rightNumberType)->yes() + $leftNumberType->isFloat()->yes() + || $rightNumberType->isFloat()->yes() ) { return new FloatType(); } @@ -1539,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 { @@ -1556,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(); } @@ -1571,12 +1564,15 @@ 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(); - } else { + } elseif ($operand instanceof ConstantIntegerType) { $operandMin = $operand->getValue(); $operandMax = $operand->getValue(); + } else { + return $operand; } if ($node instanceof BinaryOp\Plus) { @@ -1652,10 +1648,10 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T $min = min($min1, $min2, $max1, $max2); $max = max($min1, $min2, $max1, $max2); - if ($min === -INF) { + if (!is_finite($min)) { $min = null; } - if ($max === INF) { + if (!is_finite($max)) { $max = null; } } else { @@ -1878,32 +1874,60 @@ 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; } @@ -1937,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); } } 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 ed0172fda6..484690a8b7 100644 --- a/src/Reflection/Mixin/MixinMethodsClassReflectionExtension.php +++ b/src/Reflection/Mixin/MixinMethodsClassReflectionExtension.php @@ -74,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 1aae970c83..b891da3894 100644 --- a/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php +++ b/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php @@ -65,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 b2481b7411..ff52194908 100644 --- a/src/Reflection/Native/NativeFunctionReflection.php +++ b/src/Reflection/Native/NativeFunctionReflection.php @@ -17,10 +17,12 @@ class NativeFunctionReflection implements FunctionReflection /** * @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, @@ -51,6 +53,11 @@ public function getVariants(): array return $this->variants; } + public function getNamedArgumentsVariants(): ?array + { + return $this->namedArgumentsVariants; + } + public function getThrowType(): ?Type { return $this->throwType; @@ -85,6 +92,15 @@ 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) { diff --git a/src/Reflection/Native/NativeMethodReflection.php b/src/Reflection/Native/NativeMethodReflection.php index 8351b20160..9c6434286b 100644 --- a/src/Reflection/Native/NativeMethodReflection.php +++ b/src/Reflection/Native/NativeMethodReflection.php @@ -21,12 +21,14 @@ 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, @@ -56,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) { @@ -80,6 +85,7 @@ public function getPrototype(): ClassMemberReflection $prototypeMethod->isPublic(), $prototypeMethod->isAbstract(), $prototypeMethod->isFinal(), + $prototypeMethod->isInternal(), $prototypeDeclaringClass->getNativeMethod($prototypeMethod->getName())->getVariants(), $tentativeReturnType, ); @@ -98,6 +104,11 @@ 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,6 +153,15 @@ 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) { 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/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/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index ef68dc20f1..c11e767b73 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -18,9 +18,9 @@ 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; @@ -71,11 +71,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 = []; @@ -90,7 +92,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(), @@ -110,6 +112,7 @@ public static function selectFromArgs( $parameters, $acceptor->isVariadic(), $acceptor->getReturnType(), + $acceptor instanceof ParametersAcceptorWithPhpDocs ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), ), ]; } @@ -139,6 +142,7 @@ public static function selectFromArgs( $parameters, $acceptor->isVariadic(), $acceptor->getReturnType(), + $acceptor instanceof ParametersAcceptorWithPhpDocs ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), ), ]; } @@ -151,12 +155,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), ]; } } @@ -167,13 +171,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(), @@ -185,14 +192,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); @@ -215,6 +223,7 @@ public static function selectFromArgs( $parameters, $acceptor->isVariadic(), $acceptor->getReturnType(), + $acceptor instanceof ParametersAcceptorWithPhpDocs ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), ), ]; } @@ -227,9 +236,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(); @@ -238,6 +253,10 @@ public static function selectFromArgs( } } + if ($hasName && $namedArgumentsVariants !== null) { + return self::selectFromTypes($types, $namedArgumentsVariants, $unpack); + } + return self::selectFromTypes($types, $parametersAcceptors, $unpack); } @@ -248,6 +267,14 @@ private static function hasAcceptorTemplateOrLateResolvableType(ParametersAccept } foreach ($acceptor->getParameters() as $parameter) { + if ( + $parameter instanceof ParameterReflectionWithPhpDocs + && $parameter->getOutType() !== null + && self::hasTemplateOrLateResolvableType($parameter->getOutType()) + ) { + return true; + } + if (!self::hasTemplateOrLateResolvableType($parameter->getType())) { continue; } @@ -416,29 +443,16 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptorWit $parameters = []; $isVariadic = false; - $returnType = null; - $phpDocReturnType = null; - $nativeReturnType = null; + $returnTypes = []; + $phpDocReturnTypes = []; + $nativeReturnTypes = []; foreach ($acceptors as $acceptor) { - if ($returnType === null) { - $returnType = $acceptor->getReturnType(); - } else { - $returnType = TypeCombinator::union($returnType, $acceptor->getReturnType()); - } - if ($acceptor instanceof ParametersAcceptorWithPhpDocs) { - if ($phpDocReturnType === null) { - $phpDocReturnType = $acceptor->getPhpDocReturnType(); - } else { - $phpDocReturnType = TypeCombinator::union($phpDocReturnType, $acceptor->getPhpDocReturnType()); - } - } + $returnTypes[] = $acceptor->getReturnType(); + if ($acceptor instanceof ParametersAcceptorWithPhpDocs) { - if ($nativeReturnType === null) { - $nativeReturnType = $acceptor->getNativeReturnType(); - } else { - $nativeReturnType = TypeCombinator::union($nativeReturnType, $acceptor->getNativeReturnType()); - } + $phpDocReturnTypes[] = $acceptor->getPhpDocReturnType(); + $nativeReturnTypes[] = $acceptor->getNativeReturnType(); } $isVariadic = $isVariadic || $acceptor->isVariadic(); @@ -505,6 +519,10 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptorWit } } + $returnType = TypeCombinator::union(...$returnTypes); + $phpDocReturnType = $phpDocReturnTypes === [] ? null : TypeCombinator::union(...$phpDocReturnTypes); + $nativeReturnType = $nativeReturnTypes === [] ? null : TypeCombinator::union(...$nativeReturnTypes); + return new FunctionVariantWithPhpDocs( TemplateTypeMap::createEmpty(), null, @@ -530,6 +548,7 @@ private static function wrapAcceptor(ParametersAcceptor $acceptor): ParametersAc $acceptor->getReturnType(), $acceptor->getReturnType(), new MixedType(), + TemplateTypeVarianceMap::createEmpty(), ); } @@ -548,44 +567,6 @@ private static function wrapParameter(ParameterReflection $parameter): Parameter ); } - private static function getIterableValueType(Type $type): Type - { - if ($type instanceof UnionType) { - $types = []; - foreach ($type->getTypes() as $innerType) { - $iterableValueType = $innerType->getIterableValueType(); - if ($iterableValueType instanceof ErrorType) { - continue; - } - - $types[] = $iterableValueType; - } - - return TypeCombinator::union(...$types); - } - - return $type->getIterableValueType(); - } - - private static function getIterableKeyType(Type $type): Type - { - 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(); - } - private static function getCurlOptValueType(int $curlOpt): ?Type { if (defined('CURLOPT_SSL_VERIFYHOST') && $curlOpt === CURLOPT_SSL_VERIFYHOST) { @@ -785,10 +766,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/ClosureCallMethodReflection.php b/src/Reflection/Php/ClosureCallMethodReflection.php index febfeb76ab..9929386043 100644 --- a/src/Reflection/Php/ClosureCallMethodReflection.php +++ b/src/Reflection/Php/ClosureCallMethodReflection.php @@ -18,6 +18,7 @@ use PHPStan\Type\Type; use function array_map; use function array_unshift; +use function is_bool; final class ClosureCallMethodReflection implements ExtendedMethodReflection { @@ -97,10 +98,16 @@ public function getVariants(): array $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(); @@ -116,6 +123,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(); @@ -146,4 +158,19 @@ 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/EnumCasesMethodReflection.php b/src/Reflection/Php/EnumCasesMethodReflection.php index ad5e1a38dc..0665eee656 100644 --- a/src/Reflection/Php/EnumCasesMethodReflection.php +++ b/src/Reflection/Php/EnumCasesMethodReflection.php @@ -75,6 +75,11 @@ public function getVariants(): array ]; } + 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(); @@ -120,4 +130,14 @@ 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 de3bfae927..0000000000 --- a/src/Reflection/Php/FakeBuiltinMethodReflection.php +++ /dev/null @@ -1,128 +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 []; - } - - public function returnsByReference(): TrinaryLogic - { - return TrinaryLogic::createMaybe(); - } - -} diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index b04390619c..321ee288ee 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -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,7 +469,7 @@ private function createMethod( } if ($this->signatureMapProvider->hasMethodSignature($declaringClassName, $methodReflection->getName())) { - $variants = []; + $variantsByType = ['positional' => []]; $reflectionMethod = null; $throwType = null; $asserts = Assertions::createEmpty(); @@ -506,112 +478,125 @@ private function createMethod( 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; - $stubPhpParameterOutTypes = []; - $phpDocParameterOutTypes = []; - 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 = []; + 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(); + if ($returnTag !== null) { + $stubPhpDocReturnType = TemplateTypeHelper::resolveTemplateTypes( + $returnTag->getType(), + $templateTypeMap, + $callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), + ); + } - $throwsTag = $stubPhpDoc->getThrowsTag(); - if ($throwsTag !== null) { - $throwType = $throwsTag->getType(); - } + foreach ($stubPhpDoc->getParamTags() as $name => $paramTag) { + $stubPhpDocParameterTypes[$name] = TemplateTypeHelper::resolveTemplateTypes( + $paramTag->getType(), + $templateTypeMap, + $callSiteVarianceMap, + TemplateTypeVariance::createContravariant(), + ); + $stubPhpDocParameterVariadicity[$name] = $paramTag->isVariadic(); + } - $asserts = Assertions::createFromResolvedPhpDocBlock($stubPhpDoc); + $throwsTag = $stubPhpDoc->getThrowsTag(); + if ($throwsTag !== null) { + $throwType = $throwsTag->getType(); + } - $selfOutTypeTag = $stubPhpDoc->getSelfOutTag(); - if ($selfOutTypeTag !== null) { - $selfOutType = $selfOutTypeTag->getType(); - } + $asserts = Assertions::createFromResolvedPhpDocBlock($stubPhpDoc); - foreach ($stubPhpDoc->getParamOutTags() as $name => $paramOutTag) { - $stubPhpParameterOutTypes[$name] = TemplateTypeHelper::resolveTemplateTypes( - $paramOutTag->getType(), - $templateTypeMap, - ); - } + $selfOutTypeTag = $stubPhpDoc->getSelfOutTag(); + if ($selfOutTypeTag !== null) { + $selfOutType = $selfOutTypeTag->getType(); + } - 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(); - } - $asserts = Assertions::createFromResolvedPhpDocBlock($phpDocBlock); + foreach ($stubPhpDoc->getParamOutTags() as $name => $paramOutTag) { + $stubPhpParameterOutTypes[$name] = TemplateTypeHelper::resolveTemplateTypes( + $paramOutTag->getType(), + $templateTypeMap, + $callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), + ); + } - $selfOutTypeTag = $phpDocBlock->getSelfOutTag(); - if ($selfOutTypeTag !== null) { - $selfOutType = $selfOutTypeTag->getType(); + 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 && count($methodSignatures) === 1) { + $phpDocReturnType = $returnTag->getType(); + } + foreach ($phpDocBlock->getParamTags() as $name => $paramTag) { + $phpDocParameterTypes[$name] = $paramTag->getType(); + } + $asserts = Assertions::createFromResolvedPhpDocBlock($phpDocBlock); - if ($phpDocBlock->hasPhpDocString()) { - $phpDocComment = $phpDocBlock->getPhpDocString(); - } + $selfOutTypeTag = $phpDocBlock->getSelfOutTag(); + if ($selfOutTypeTag !== null) { + $selfOutType = $selfOutTypeTag->getType(); + } - foreach ($phpDocBlock->getParamOutTags() as $name => $paramOutTag) { - $phpDocParameterOutTypes[$name] = $paramOutTag->getType(); - } + if ($phpDocBlock->hasPhpDocString()) { + $phpDocComment = $phpDocBlock->getPhpDocString(); + } - $signatureParameters = $methodSignature->getParameters(); - foreach ($reflectionMethod->getParameters() as $paramI => $reflectionParameter) { - if (!array_key_exists($paramI, $signatureParameters)) { - continue; + foreach ($phpDocBlock->getParamOutTags() as $name => $paramOutTag) { + $phpDocParameterOutTypes[$name] = $paramOutTag->getType(); } - $phpDocParameterNameMapping[$signatureParameters[$paramI]->getName()] = $reflectionParameter->getName(); + $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, $signatureType !== 'named'); } - $variants[] = $this->createNativeMethodVariant($methodSignature, $stubPhpDocParameterTypes, $stubPhpDocParameterVariadicity, $stubPhpDocReturnType, $phpDocParameterTypes, $phpDocReturnType, $phpDocParameterNameMapping, $stubPhpParameterOutTypes, $phpDocParameterOutTypes); } if ($this->signatureMapProvider->hasMethodMetadata($declaringClassName, $methodReflection->getName())) { @@ -623,7 +608,8 @@ private function createMethod( $this->reflectionProviderProvider->getReflectionProvider(), $declaringClass, $methodReflection, - $variants, + $variantsByType['positional'], + $variantsByType['named'] ?? null, $hasSideEffects, $throwType, $asserts, @@ -632,10 +618,19 @@ private function createMethod( ); } - $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(); @@ -664,13 +659,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; @@ -704,8 +699,8 @@ private function createMethod( } $propertyDocblock = $this->fileTypeMapper->getResolvedPhpDoc( - $declaringClass->getFileName(), - $declaringClassName, + $fileDeclaringClass->getFileName(), + $fileDeclaringClass->getName(), $declaringTraitName, $methodReflection->getName(), $parameterProperty->getDocComment(), @@ -735,6 +730,8 @@ private function createMethod( $phpDocParameterTypes[$paramName] = TemplateTypeHelper::resolveTemplateTypes( $paramType, $phpDocBlockClassReflection->getActiveTemplateTypeMap(), + $phpDocBlockClassReflection->getCallSiteVarianceMap(), + TemplateTypeVariance::createContravariant(), ); } @@ -743,13 +740,15 @@ private function createMethod( $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; @@ -766,7 +765,7 @@ private function createMethod( } return $this->methodReflectionFactory->create( - $declaringClass, + $actualDeclaringClass, $declaringTrait, $methodReflection, $templateTypeMap, @@ -803,6 +802,7 @@ private function createNativeMethodVariant( array $phpDocParameterNameMapping, array $stubPhpDocParameterOutTypes, array $phpDocParameterOutTypes, + bool $usePhpDocParameterNames, ): FunctionVariantWithPhpDocs { $parameters = []; @@ -827,7 +827,9 @@ private function createNativeMethodVariant( } $parameters[] = new NativeParameterWithPhpDocsReflection( - $phpDocParameterName, + $usePhpDocParameterNames + ? $phpDocParameterName + : $parameterSignature->getName(), $parameterSignature->isOptional(), $type ?? $parameterSignature->getType(), $phpDocType ?? new MixedType(), @@ -839,10 +841,11 @@ private function createNativeMethodVariant( ); } - $returnType = null; if ($stubPhpDocReturnType !== null) { $returnType = $stubPhpDocReturnType; $phpDocReturnType = $stubPhpDocReturnType; + } else { + $returnType = TypehintHelper::decideType($methodSignature->getReturnType(), $phpDocReturnType); } return new FunctionVariantWithPhpDocs( @@ -850,7 +853,7 @@ private function createNativeMethodVariant( null, $parameters, $methodSignature->isVariadic(), - $returnType ?? $methodSignature->getReturnType(), + $returnType, $phpDocReturnType ?? new MixedType(), $methodSignature->getNativeReturnType(), ); @@ -1069,6 +1072,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 1ef557770c..45044190e4 100644 --- a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php @@ -111,6 +111,11 @@ public function getVariants(): array return $this->variants; } + public function getNamedArgumentsVariants(): ?array + { + return null; + } + /** * @return ParameterReflectionWithPhpDocs[] */ @@ -189,6 +194,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; @@ -267,4 +281,13 @@ 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 2627fdd927..927b8610fd 100644 --- a/src/Reflection/Php/PhpFunctionReflection.php +++ b/src/Reflection/Php/PhpFunctionReflection.php @@ -103,6 +103,11 @@ public function getVariants(): array return $this->variants; } + public function getNamedArgumentsVariants(): ?array + { + return null; + } + /** * @return ParameterReflectionWithPhpDocs[] */ @@ -249,6 +254,15 @@ 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(); diff --git a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php index 0cd43a249d..f04d2bac38 100644 --- a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php @@ -14,11 +14,13 @@ 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; /** @@ -56,13 +58,7 @@ public function __construct( ) { $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') { @@ -77,6 +73,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, @@ -151,4 +163,9 @@ public function returnsByReference(): TrinaryLogic return TrinaryLogic::createFromBoolean($this->getClassMethod()->returnsByRef()); } + public function isAbstract(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->getClassMethod()->isAbstract()); + } + } diff --git a/src/Reflection/Php/PhpMethodReflection.php b/src/Reflection/Php/PhpMethodReflection.php index 62d7b1312a..f28d27216b 100644 --- a/src/Reflection/Php/PhpMethodReflection.php +++ b/src/Reflection/Php/PhpMethodReflection.php @@ -30,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; @@ -37,6 +38,7 @@ use function array_map; use function explode; use function filemtime; +use function in_array; use function is_bool; use function sprintf; use function strtolower; @@ -104,7 +106,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 +124,7 @@ public function getPrototype(): ClassMemberReflection $prototypeMethod->isPublic(), $prototypeMethod->isAbstract(), $prototypeMethod->isFinal(), + $prototypeMethod->isInternal(), $prototypeDeclaringClass->getNativeMethod($prototypeMethod->getName())->getVariants(), $tentativeReturnType, ); @@ -194,6 +200,11 @@ public function getVariants(): array return $this->variants; } + public function getNamedArgumentsVariants(): ?array + { + return null; + } + /** * @return ParameterReflectionWithPhpDocs[] */ @@ -316,13 +327,7 @@ private function getReturnType(): Type $name = strtolower($this->getName()); $returnType = $this->reflection->getReturnType(); if ($returnType === null) { - if ( - $name === '__construct' - || $name === '__destruct' - || $name === '__unset' - || $name === '__wakeup' - || $name === '__clone' - ) { + if (in_array($name, ['__construct', '__destruct', '__unset', '__wakeup', '__clone'], true)) { return $this->returnType = TypehintHelper::decideType(new VoidType(), $this->phpDocReturnType); } if ($name === '__tostring') { @@ -342,7 +347,7 @@ private function getReturnType(): Type $this->returnType = TypehintHelper::decideTypeFromReflection( $returnType, $this->phpDocReturnType, - $this->declaringClass->getName(), + $this->declaringClass, ); } @@ -364,7 +369,7 @@ private function getNativeReturnType(): Type $this->nativeReturnType = TypehintHelper::decideTypeFromReflection( $this->reflection->getReturnType(), null, - $this->declaringClass->getName(), + $this->declaringClass, ); } @@ -382,17 +387,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 @@ -417,6 +431,10 @@ public function hasSideEffects(): TrinaryLogic return TrinaryLogic::createFromBoolean(!$this->isPure); } + if ((new ThisType($this->declaringClass))->isSuperTypeOf($this->getReturnType())->yes()) { + return TrinaryLogic::createYes(); + } + return TrinaryLogic::createMaybe(); } @@ -440,4 +458,13 @@ 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/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 d040e7a49e..b618cf3928 100644 --- a/src/Reflection/ResolvedFunctionVariant.php +++ b/src/Reflection/ResolvedFunctionVariant.php @@ -2,12 +2,17 @@ namespace PHPStan\Reflection; +use PHPStan\DependencyInjection\BleedingEdgeToggle; use PHPStan\Reflection\Php\DummyParameterWithPhpDocs; use PHPStan\Type\ConditionalTypeForParameter; use PHPStan\Type\ErrorType; +use PHPStan\Type\Generic\GenericObjectType; 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\NonAcceptingNeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; @@ -34,6 +39,7 @@ class ResolvedFunctionVariant implements ParametersAcceptorWithPhpDocs public function __construct( private ParametersAcceptorWithPhpDocs $parametersAcceptor, private TemplateTypeMap $resolvedTemplateTypeMap, + private TemplateTypeVarianceMap $callSiteVarianceMap, private array $passedArgs, ) { @@ -54,6 +60,11 @@ public function getResolvedTemplateTypeMap(): TemplateTypeMap return $this->resolvedTemplateTypeMap; } + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return $this->callSiteVarianceMap; + } + public function getParameters(): array { $parameters = $this->parameters; @@ -65,10 +76,25 @@ function (ParameterReflectionWithPhpDocs $param): ParameterReflectionWithPhpDocs 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, + ); + } + return new DummyParameterWithPhpDocs( $param->getName(), $paramType, @@ -78,7 +104,7 @@ function (ParameterReflectionWithPhpDocs $param): ParameterReflectionWithPhpDocs $param->getDefaultValue(), $param->getNativeType(), $param->getPhpDocType(), - $param->getOutType(), + $paramOutType, ); }, $this->parametersAcceptor->getParameters(), @@ -99,7 +125,7 @@ public function getReturnTypeWithUnresolvableTemplateTypes(): Type { return $this->returnTypeWithUnresolvableTemplateTypes ??= $this->resolveConditionalTypesForParameter( - $this->resolveResolvableTemplateTypes($this->parametersAcceptor->getReturnType()), + $this->resolveResolvableTemplateTypes($this->parametersAcceptor->getReturnType(), TemplateTypeVariance::createCovariant()), ); } @@ -107,7 +133,7 @@ public function getPhpDocReturnTypeWithUnresolvableTemplateTypes(): Type { return $this->phpDocReturnTypeWithUnresolvableTemplateTypes ??= $this->resolveConditionalTypesForParameter( - $this->resolveResolvableTemplateTypes($this->parametersAcceptor->getPhpDocReturnType()), + $this->resolveResolvableTemplateTypes($this->parametersAcceptor->getPhpDocReturnType(), TemplateTypeVariance::createCovariant()), ); } @@ -120,6 +146,8 @@ public function getReturnType(): Type TemplateTypeHelper::resolveTemplateTypes( $this->getReturnTypeWithUnresolvableTemplateTypes(), $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), ), false, ); @@ -139,6 +167,8 @@ public function getPhpDocReturnType(): Type TemplateTypeHelper::resolveTemplateTypes( $this->getPhpDocReturnTypeWithUnresolvableTemplateTypes(), $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createCovariant(), ), false, ); @@ -154,14 +184,86 @@ public function getNativeReturnType(): Type return $this->parametersAcceptor->getNativeReturnType(); } - private function resolveResolvableTemplateTypes(Type $type): Type + private function resolveResolvableTemplateTypes(Type $type, TemplateTypeVariance $positionVariance): Type { - return TypeTraverser::map($type, function (Type $type, callable $traverse): 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) { + 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); diff --git a/src/Reflection/ResolvedMethodReflection.php b/src/Reflection/ResolvedMethodReflection.php index 719d5cabc6..d0a890c79a 100644 --- a/src/Reflection/ResolvedMethodReflection.php +++ b/src/Reflection/ResolvedMethodReflection.php @@ -6,7 +6,10 @@ 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 ExtendedMethodReflection { @@ -14,11 +17,18 @@ class ResolvedMethodReflection implements ExtendedMethodReflection /** @var ParametersAcceptorWithPhpDocs[]|null */ private ?array $variants = null; + /** @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) + public function __construct( + private ExtendedMethodReflection $reflection, + private TemplateTypeMap $resolvedTemplateTypeMap, + private TemplateTypeVarianceMap $callSiteVarianceMap, + ) { } @@ -39,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 ResolvedFunctionVariant( $variant, $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, [], ); } - $this->variants = $variants; - - return $variants; + return $result; } public function getDeclaringClass(): ClassReflection @@ -102,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(); @@ -117,9 +155,19 @@ public function hasSideEffects(): TrinaryLogic return $this->reflection->hasSideEffects(); } + public function isPure(): TrinaryLogic + { + return $this->reflection->hasSideEffects(); + } + public function getAsserts(): Assertions { - return $this->asserts ??= $this->reflection->getAsserts()->mapTypes(fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes($type, $this->resolvedTemplateTypeMap)); + return $this->asserts ??= $this->reflection->getAsserts()->mapTypes(fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( + $type, + $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createInvariant(), + )); } public function getSelfOutType(): ?Type @@ -127,7 +175,12 @@ public function getSelfOutType(): ?Type if ($this->selfOutType === false) { $selfOutType = $this->reflection->getSelfOutType(); if ($selfOutType !== null) { - $selfOutType = TemplateTypeHelper::resolveTemplateTypes($selfOutType, $this->resolvedTemplateTypeMap); + $selfOutType = TemplateTypeHelper::resolveTemplateTypes( + $selfOutType, + $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + TemplateTypeVariance::createInvariant(), + ); } $this->selfOutType = $selfOutType; @@ -141,4 +194,14 @@ 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 d4ce2b06af..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 @@ -125,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); } @@ -148,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; } /** @@ -170,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; } /** @@ -237,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 56f3a6bcc5..734bf96f7d 100644 --- a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php +++ b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php @@ -13,19 +13,11 @@ use PHPStan\Reflection\Native\NativeFunctionReflection; 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\MixedType; -use PHPStan\Type\NullType; -use PHPStan\Type\StringAlwaysAcceptingObjectWithToStringType; -use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; -use PHPStan\Type\UnionType; use function array_key_exists; use function array_map; use function strtolower; @@ -43,6 +35,7 @@ public function __construct(private SignatureMapProvider $signatureMapProvider, public function findFunctionReflection(string $functionName): ?NativeFunctionReflection { $lowerCasedFunctionName = strtolower($functionName); + $realFunctionName = $lowerCasedFunctionName; if (isset($this->functionMap[$lowerCasedFunctionName])) { return $this->functionMap[$lowerCasedFunctionName]; } @@ -62,6 +55,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $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(); @@ -78,9 +72,9 @@ 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())); + $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(); @@ -92,74 +86,41 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $phpDocReturnType = $this->getReturnTypeFromPhpDoc($phpDoc); } - $variants = []; - $functionSignatures = $this->signatureMapProvider->getFunctionSignatures($lowerCasedFunctionName, null, $reflectionFunctionAdapter); - foreach ($functionSignatures as $functionSignature) { - $variants[] = new FunctionVariantWithPhpDocs( - TemplateTypeMap::createEmpty(), - null, - array_map(static function (ParameterSignature $parameterSignature) use ($lowerCasedFunctionName, $phpDoc): NativeParameterWithPhpDocsReflection { - $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; + if ($phpDoc !== null) { + $phpDocParam = $phpDoc->getParamTags()[$parameterSignature->getName()] ?? null; + if ($phpDocParam !== null) { + $phpDocType = $phpDocParam->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, ); - } - - 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, - ); - }, $functionSignature->getParameters()), - $functionSignature->isVariadic(), - TypehintHelper::decideType($functionSignature->getReturnType(), $phpDocReturnType), - $phpDocReturnType ?? new MixedType(), - $functionSignature->getReturnType(), - ); + }, $functionSignature->getParameters()), + $functionSignature->isVariadic(), + TypehintHelper::decideType($functionSignature->getReturnType(), $phpDocReturnType), + $phpDocReturnType ?? new MixedType(), + $functionSignature->getReturnType(), + ); + } } if ($this->signatureMapProvider->hasFunctionMetadata($lowerCasedFunctionName)) { @@ -169,8 +130,9 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef } $functionReflection = new NativeFunctionReflection( - $lowerCasedFunctionName, - $variants, + $realFunctionName, + $variantsByType['positional'], + $variantsByType['named'] ?? null, $throwType, $hasSideEffects, $isDeprecated, diff --git a/src/Reflection/SignatureMap/Php8SignatureMapProvider.php b/src/Reflection/SignatureMap/Php8SignatureMapProvider.php index 50b65d949b..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(), + ); + } + + if ($allParamNamesMatchNative || count($namedArgumentsVariants) === 0) { + $namedArgumentsVariants = null; } - return $functionMapSignatures; + return ['positional' => $functionMapSignatures['positional'], 'named' => $namedArgumentsVariants]; } private function mergeSignatures(FunctionSignature $nativeSignature, FunctionSignature $functionMapSignature): FunctionSignature @@ -359,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 9d79f9b354..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 @@ -95,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 fca6afe615..746b0582a0 100644 --- a/src/Reflection/TrivialParametersAcceptor.php +++ b/src/Reflection/TrivialParametersAcceptor.php @@ -3,6 +3,7 @@ namespace PHPStan\Reflection; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\MixedType; use PHPStan\Type\Type; @@ -25,6 +26,11 @@ public function getResolvedTemplateTypeMap(): TemplateTypeMap return TemplateTypeMap::createEmpty(); } + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return TemplateTypeVarianceMap::createEmpty(); + } + public function getParameters(): array { return []; diff --git a/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php index 6c2f87ab04..61d7a2f00b 100644 --- a/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php @@ -61,10 +61,12 @@ public function getTransformedMethod(): ExtendedMethodReflection 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, ); } @@ -80,7 +82,7 @@ public function withCalledOnType(Type $type): UnresolvedMethodPrototypeReflectio private function transformMethodWithStaticType(ClassReflection $declaringClass, ExtendedMethodReflection $method): ExtendedMethodReflection { - $variants = array_map(fn (ParametersAcceptorWithPhpDocs $acceptor): ParametersAcceptorWithPhpDocs => new FunctionVariantWithPhpDocs( + $variantFn = fn (ParametersAcceptorWithPhpDocs $acceptor): ParametersAcceptorWithPhpDocs => new FunctionVariantWithPhpDocs( $acceptor->getTemplateTypeMap(), $acceptor->getResolvedTemplateTypeMap(), array_map( @@ -101,9 +103,15 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass, $this->transformStaticType($acceptor->getReturnType()), $this->transformStaticType($acceptor->getPhpDocReturnType()), $this->transformStaticType($acceptor->getNativeReturnType()), - ), $method->getVariants()); + $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 c3b9f05737..a76cf064aa 100644 --- a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php @@ -56,10 +56,12 @@ public function getTransformedMethod(): ExtendedMethodReflection 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, ); } @@ -75,7 +77,7 @@ public function withCalledOnType(Type $type): UnresolvedMethodPrototypeReflectio private function transformMethodWithStaticType(ClassReflection $declaringClass, ExtendedMethodReflection $method): ExtendedMethodReflection { - $variants = array_map(fn (ParametersAcceptorWithPhpDocs $acceptor): ParametersAcceptorWithPhpDocs => new FunctionVariantWithPhpDocs( + $variantFn = fn (ParametersAcceptorWithPhpDocs $acceptor): ParametersAcceptorWithPhpDocs => new FunctionVariantWithPhpDocs( $acceptor->getTemplateTypeMap(), $acceptor->getResolvedTemplateTypeMap(), array_map( @@ -96,9 +98,15 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass, $this->transformStaticType($acceptor->getReturnType()), $this->transformStaticType($acceptor->getPhpDocReturnType()), $this->transformStaticType($acceptor->getNativeReturnType()), - ), $method->getVariants()); + $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 b752a7502a..fb9a8ecdd3 100644 --- a/src/Reflection/Type/IntersectionTypeMethodReflection.php +++ b/src/Reflection/Type/IntersectionTypeMethodReflection.php @@ -16,6 +16,7 @@ use function array_map; use function count; use function implode; +use function is_bool; class IntersectionTypeMethodReflection implements ExtendedMethodReflection { @@ -89,9 +90,15 @@ public function getVariants(): array $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()); @@ -124,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()); @@ -154,6 +166,11 @@ 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; @@ -180,4 +197,9 @@ 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/UnionTypeMethodReflection.php b/src/Reflection/Type/UnionTypeMethodReflection.php index 0168a315b2..65ca5bd674 100644 --- a/src/Reflection/Type/UnionTypeMethodReflection.php +++ b/src/Reflection/Type/UnionTypeMethodReflection.php @@ -15,6 +15,7 @@ use function array_merge; use function count; use function implode; +use function is_bool; class UnionTypeMethodReflection implements ExtendedMethodReflection { @@ -81,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()); @@ -113,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()); @@ -143,6 +154,11 @@ 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; @@ -163,4 +179,9 @@ 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/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/WrappedExtendedMethodReflection.php b/src/Reflection/WrappedExtendedMethodReflection.php index 8ac11e6996..dd083d9055 100644 --- a/src/Reflection/WrappedExtendedMethodReflection.php +++ b/src/Reflection/WrappedExtendedMethodReflection.php @@ -4,6 +4,7 @@ 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; @@ -77,12 +78,18 @@ public function getVariants(): array $variant->getReturnType(), $variant->getReturnType(), new MixedType(), + TemplateTypeVarianceMap::createEmpty(), ); } return $variants; } + public function getNamedArgumentsVariants(): ?array + { + return null; + } + public function isDeprecated(): TrinaryLogic { return $this->method->isDeprecated(); @@ -98,6 +105,11 @@ public function isFinal(): TrinaryLogic return $this->method->isFinal(); } + public function isFinalByKeyword(): TrinaryLogic + { + return $this->isFinal(); + } + public function isInternal(): TrinaryLogic { return $this->method->isInternal(); @@ -113,6 +125,11 @@ public function hasSideEffects(): TrinaryLogic return $this->method->hasSideEffects(); } + public function isPure(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function getAsserts(): Assertions { return Assertions::createEmpty(); @@ -128,4 +145,9 @@ 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..c5ec413081 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 @@ -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/ApiInstanceofTypeRule.php b/src/Rules/Api/ApiInstanceofTypeRule.php index 16d1891135..2ab5194ca6 100644 --- a/src/Rules/Api/ApiInstanceofTypeRule.php +++ b/src/Rules/Api/ApiInstanceofTypeRule.php @@ -25,12 +25,19 @@ 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\ConstantScalarType; +use PHPStan\Type\ConstantType; use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\FloatType; +use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\IntegerType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\IterableType; use PHPStan\Type\NullType; +use PHPStan\Type\ObjectShapeType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; @@ -47,10 +54,10 @@ class ApiInstanceofTypeRule implements Rule { private const MAP = [ - TypeWithClassName::class => 'Type::getObjectClassNames()', + TypeWithClassName::class => 'Type::getObjectClassNames() or Type::getObjectClassReflections()', EnumCaseObjectType::class => 'Type::getEnumCases()', ConstantArrayType::class => 'Type::getConstantArrays()', - ArrayType::class => 'Type::getArrays()', + ArrayType::class => 'Type::isArray() or Type::getArrays()', ConstantStringType::class => 'Type::getConstantStrings()', StringType::class => 'Type::isString()', ClassStringType::class => 'Type::isClassStringType()', @@ -59,11 +66,17 @@ class ApiInstanceofTypeRule implements Rule NullType::class => 'Type::isNull()', VoidType::class => 'Type::isVoid()', BooleanType::class => 'Type::isBoolean()', - // ConstantBooleanType::class => 'Type::isTrue() or Type::isFalse()', skipped because not that valuable + 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()', @@ -126,12 +139,22 @@ public function processNode(Node $node, Scope $scope): array } } + $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, + ))->tip($tip)->build(), + ]; + } + return [ RuleErrorBuilder::message(sprintf( 'Doing instanceof %s is error-prone and deprecated. Use %s instead.', $className, $lowerMap[$lowerClassName], - ))->build(), + ))->tip($tip)->build(), ]; } diff --git a/src/Rules/Api/ApiInstantiationRule.php b/src/Rules/Api/ApiInstantiationRule.php index 9c4cc7e5bb..d00d0b9520 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 @@ -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/ApiMethodCallRule.php b/src/Rules/Api/ApiMethodCallRule.php index 540f834f00..bc98f39c45 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 @@ -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..d0176dc9d4 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 @@ -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/BcUncoveredInterface.php b/src/Rules/Api/BcUncoveredInterface.php index e3875170cf..795f5f8f7d 100644 --- a/src/Rules/Api/BcUncoveredInterface.php +++ b/src/Rules/Api/BcUncoveredInterface.php @@ -8,6 +8,13 @@ use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; use PHPStan\Type\Type; final class BcUncoveredInterface @@ -21,6 +28,13 @@ final class BcUncoveredInterface ExtendedMethodReflection::class, ParametersAcceptorWithPhpDocs::class, ParameterReflectionWithPhpDocs::class, + FileRuleError::class, + IdentifierRuleError::class, + LineRuleError::class, + MetadataRuleError::class, + NonIgnorableRuleError::class, + RuleError::class, + TipRuleError::class, ]; } diff --git a/src/Rules/Api/NodeConnectingVisitorAttributesRule.php b/src/Rules/Api/NodeConnectingVisitorAttributesRule.php index 3db9885335..8fab985aed 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; } diff --git a/src/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRule.php b/src/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRule.php index 892bbc1660..1810edb23a 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,7 +46,7 @@ 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 []; } diff --git a/src/Rules/Api/RuntimeReflectionFunctionRule.php b/src/Rules/Api/RuntimeReflectionFunctionRule.php index d71a62d1eb..94f083835d 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; } diff --git a/src/Rules/Api/RuntimeReflectionInstantiationRule.php b/src/Rules/Api/RuntimeReflectionInstantiationRule.php index 24a7579af2..c7845b09dd 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; } diff --git a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php index e444bc5268..9cb8f8f4a5 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()) { @@ -87,7 +91,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - if ($dimType === null || $scope->hasExpressionType($node)->yes()) { + if ($dimType === null) { return []; } diff --git a/src/Rules/AttributesCheck.php b/src/Rules/AttributesCheck.php index 05b38c0562..41e8132e89 100644 --- a/src/Rules/AttributesCheck.php +++ b/src/Rules/AttributesCheck.php @@ -20,7 +20,7 @@ class AttributesCheck public function __construct( private ReflectionProvider $reflectionProvider, private FunctionCallParametersCheck $functionCallParametersCheck, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private bool $deprecationRulesInstalled, ) { @@ -28,7 +28,7 @@ public function __construct( /** * @param AttributeGroup[] $attrGroups - * @param Attribute::TARGET_* $requiredTarget + * @param int-mask-of $requiredTarget * @return RuleError[] */ public function check( @@ -67,7 +67,7 @@ public function check( $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s is abstract.', $name))->line($attribute->getLine())->build(); } - foreach ($this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($name, $attribute)]) as $caseSensitivityError) { + foreach ($this->classCheck->checkClassNames([new ClassNameNodePair($name, $attribute)]) as $caseSensitivityError) { $errors[] = $caseSensitivityError; } @@ -116,6 +116,7 @@ public function check( $scope, $attribute->args, $attributeConstructor->getVariants(), + $attributeConstructor->getNamedArgumentsVariants(), ), $scope, $attributeConstructor->getDeclaringClass()->isBuiltin(), diff --git a/src/Rules/ClassForbiddenNameCheck.php b/src/Rules/ClassForbiddenNameCheck.php new file mode 100644 index 0000000000..8dfe6ff141 --- /dev/null +++ b/src/Rules/ClassForbiddenNameCheck.php @@ -0,0 +1,89 @@ + '_PHPStan_', + 'Rector' => 'RectorPrefix', + 'PHP-Scoper' => '_PhpScoper', + 'PHPUnit' => 'PHPUnitPHAR', + ]; + + public function __construct(private Container $container) + { + } + + /** + * @param ClassNameNodePair[] $pairs + * @return RuleError[] + */ + 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())->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..fd194bb399 --- /dev/null +++ b/src/Rules/ClassNameCheck.php @@ -0,0 +1,35 @@ +classCaseSensitivityCheck->checkClassNames($pairs) as $error) { + $errors[] = $error; + } + } + foreach ($this->classForbiddenNameCheck->checkClassNames($pairs) as $error) { + $errors[] = $error; + } + + return $errors; + } + +} diff --git a/src/Rules/Classes/ClassConstantRule.php b/src/Rules/Classes/ClassConstantRule.php index 54d317a483..5f4a2fa96a 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, ) { @@ -99,10 +99,10 @@ public function processNode(Node $node, Scope $scope): array sprintf('Access to constant %s on an unknown class %s.', $constantName, $className), )->discoveringSymbolsTip()->build(), ]; - } else { - $messages = $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($className, $class)]); } + $messages = $this->classCheck->checkClassNames([new ClassNameNodePair($className, $class)]); + $classType = $scope->resolveTypeByName($class); } diff --git a/src/Rules/Classes/DuplicateDeclarationRule.php b/src/Rules/Classes/DuplicateDeclarationRule.php index 257bf2eaf7..64bda42406 100644 --- a/src/Rules/Classes/DuplicateDeclarationRule.php +++ b/src/Rules/Classes/DuplicateDeclarationRule.php @@ -28,10 +28,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $classReflection = $scope->getClassReflection(); - if ($classReflection === null) { - throw new ShouldNotHappenException(); - } + $classReflection = $node->getClassReflection(); $errors = []; diff --git a/src/Rules/Classes/EnumSanityRule.php b/src/Rules/Classes/EnumSanityRule.php index aa94004670..53abe5d098 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,33 +29,28 @@ 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(); } @@ -61,17 +61,17 @@ public function processNode(Node $node, Scope $scope): array if ($lowercasedMethodName === '__construct') { $errors[] = RuleErrorBuilder::message(sprintf( 'Enum %s contains constructor.', - $node->namespacedName->toString(), + $classReflection->getDisplayName(), ))->line($methodNode->getLine())->nonIgnorable()->build(); } elseif ($lowercasedMethodName === '__destruct') { $errors[] = RuleErrorBuilder::message(sprintf( 'Enum %s contains destructor.', - $node->namespacedName->toString(), + $classReflection->getDisplayName(), ))->line($methodNode->getLine())->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(); } @@ -80,46 +80,125 @@ public function processNode(Node $node, Scope $scope): array 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(); } - 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(); } 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(), + ))->line($enumNode->scalarType->getLine())->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(), + ))->line($enumNode->getLine())->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->getLine()) + ->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->getLine()) + ->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->getLine()) + ->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->getLine()) + ->nonIgnorable() + ->build(); } return $errors; diff --git a/src/Rules/Classes/ExistingClassInClassExtendsRule.php b/src/Rules/Classes/ExistingClassInClassExtendsRule.php index 513c7efcd5..b24e62eb5f 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; @@ -81,6 +81,24 @@ public function processNode(Node $node, Scope $scope): array $reflection->getDisplayName(), ))->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(), + ))->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(), + ))->nonIgnorable()->build(); + } + } } return $messages; diff --git a/src/Rules/Classes/ExistingClassInInstanceOfRule.php b/src/Rules/Classes/ExistingClassInInstanceOfRule.php index 293dd6285d..da4d7a098b 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, ) { @@ -69,13 +69,16 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message(sprintf('Class %s not found.', $name))->line($class->getLine())->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); if ($classReflection->isTrait()) { diff --git a/src/Rules/Classes/ExistingClassInTraitUseRule.php b/src/Rules/Classes/ExistingClassInTraitUseRule.php index 43e84d80aa..d81ca9b552 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), ); diff --git a/src/Rules/Classes/ExistingClassesInClassImplementsRule.php b/src/Rules/Classes/ExistingClassesInClassImplementsRule.php index fde6d21979..6739218989 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), ); diff --git a/src/Rules/Classes/ExistingClassesInEnumImplementsRule.php b/src/Rules/Classes/ExistingClassesInEnumImplementsRule.php index 81f6168eba..aff605fe99 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), ); diff --git a/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php b/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php index f267550104..40355184ab 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), ); diff --git a/src/Rules/Classes/ImpossibleInstanceOfRule.php b/src/Rules/Classes/ImpossibleInstanceOfRule.php index c64c773206..c84fd5400e 100644 --- a/src/Rules/Classes/ImpossibleInstanceOfRule.php +++ b/src/Rules/Classes/ImpossibleInstanceOfRule.php @@ -24,6 +24,7 @@ class ImpossibleInstanceOfRule implements Rule public function __construct( private bool $checkAlwaysTrueInstanceof, private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, ) { } @@ -84,19 +85,22 @@ public function processNode(Node $node, Scope $scope): array )))->build(), ]; } elseif ($this->checkAlwaysTrueInstanceof) { - if ($node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME) === true) { + $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.'); + } - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Instanceof between %s and %s will always evaluate to true.', - $exprType->describe(VerbosityLevel::typeOnly()), - $classType->describe(VerbosityLevel::getRecommendedLevelByType($classType)), - )))->build(), - ]; + return [$errorBuilder->build()]; } return []; diff --git a/src/Rules/Classes/InstantiationRule.php b/src/Rules/Classes/InstantiationRule.php index 93886fa8d8..16724e1357 100644 --- a/src/Rules/Classes/InstantiationRule.php +++ b/src/Rules/Classes/InstantiationRule.php @@ -9,7 +9,7 @@ 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\Rule; @@ -32,7 +32,7 @@ class InstantiationRule implements Rule public function __construct( private ReflectionProvider $reflectionProvider, private FunctionCallParametersCheck $check, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, ) { } @@ -117,12 +117,12 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ return [ RuleErrorBuilder::message(sprintf('Instantiated class %s not found.', $class))->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); } @@ -185,6 +185,7 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ $scope, $node->getArgs(), $constructorReflection->getVariants(), + $constructorReflection->getNamedArgumentsVariants(), ), $scope, $constructorReflection->getDeclaringClass()->isBuiltin(), diff --git a/src/Rules/Classes/InvalidPromotedPropertiesRule.php b/src/Rules/Classes/InvalidPromotedPropertiesRule.php index b81e387b60..55f7aa133a 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; } @@ -61,7 +53,9 @@ public function processNode(Node $node, Scope $scope): array if ( !$node instanceof Node\Stmt\ClassMethod - || $node->name->toLowerString() !== '__construct' + || ( + $node->name->toLowerString() !== '__construct' + && $node->getAttribute('originalTraitMethodName') !== '__construct') ) { return [ RuleErrorBuilder::message( @@ -70,7 +64,7 @@ public function processNode(Node $node, Scope $scope): array ]; } - if ($node->stmts === null) { + if ($node->getStmts() === null) { return [ RuleErrorBuilder::message( 'Promoted properties are not allowed in abstract constructors.', @@ -79,7 +73,7 @@ public function processNode(Node $node, Scope $scope): array } $errors = []; - foreach ($node->params as $param) { + foreach ($node->getParams() as $param) { if ($param->flags === 0) { continue; } diff --git a/src/Rules/Classes/LocalTypeAliasesCheck.php b/src/Rules/Classes/LocalTypeAliasesCheck.php new file mode 100644 index 0000000000..de1c0312aa --- /dev/null +++ b/src/Rules/Classes/LocalTypeAliasesCheck.php @@ -0,0 +1,176 @@ + $globalTypeAliases + */ + public function __construct( + private array $globalTypeAliases, + private ReflectionProvider $reflectionProvider, + private TypeNodeResolver $typeNodeResolver, + ) + { + } + + /** + * @return RuleError[] + */ + 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))->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->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 bbdf8af395..86697971b8 100644 --- a/src/Rules/Classes/LocalTypeAliasesRule.php +++ b/src/Rules/Classes/LocalTypeAliasesRule.php @@ -3,22 +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\Type; -use PHPStan\Type\TypeTraverser; -use function array_key_exists; -use function in_array; -use function sprintf; /** * @implements Rule @@ -26,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) { } @@ -44,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->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 + 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..ee58456983 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,10 +42,7 @@ 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) { @@ -68,6 +65,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]) { @@ -83,12 +82,12 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @mixin contains unknown class %s.', $class))->discoveringSymbolsTip()->build(); } elseif ($this->reflectionProvider->getClass($class)->isTrait()) { $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @mixin contains invalid type %s.', $class))->build(); - } elseif ($this->checkClassCaseSensitivity) { + } else { $errors = array_merge( $errors, - $this->classCaseSensitivityCheck->checkClassNames([ + $this->classCheck->checkClassNames([ new ClassNameNodePair($class, $node), - ]), + ], $this->checkClassCaseSensitivity), ); } } 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..a2f59e7b51 --- /dev/null +++ b/src/Rules/Classes/RequireExtendsRule.php @@ -0,0 +1,83 @@ + + */ +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(), + ), + )->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(), + ), + )->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/Classes/RequireImplementsRule.php b/src/Rules/Classes/RequireImplementsRule.php new file mode 100644 index 0000000000..551cdd3ed2 --- /dev/null +++ b/src/Rules/Classes/RequireImplementsRule.php @@ -0,0 +1,56 @@ + + */ +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(), + ), + )->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/Comparison/BooleanAndConstantConditionRule.php b/src/Rules/Comparison/BooleanAndConstantConditionRule.php index 3114bde615..bc1c262055 100644 --- a/src/Rules/Comparison/BooleanAndConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanAndConstantConditionRule.php @@ -22,6 +22,7 @@ public function __construct( private ConstantConditionRuleHelper $helper, private bool $treatPhpDocTypesAsCertain, private bool $bleedingEdge, + private bool $reportAlwaysTrueInLastCondition, ) { } @@ -55,12 +56,17 @@ public function processNode( return $ruleErrorBuilder->tip($tipText); }; - if ($leftType->getValue() === false || $originalNode->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME) !== true) { - $errors[] = $addTipLeft(RuleErrorBuilder::message(sprintf( + $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', - )))->line($originalNode->left->getLine())->build(); + )))->line($originalNode->left->getLine()); + if ($leftType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + $errors[] = $errorBuilder->build(); } } @@ -86,12 +92,17 @@ public function processNode( return $ruleErrorBuilder->tip($tipText); }; - if ($rightType->getValue() === false || $originalNode->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME) !== true) { - $errors[] = $addTipRight(RuleErrorBuilder::message(sprintf( + $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(); + )))->line($originalNode->right->getLine()); + if ($rightType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + $errors[] = $errorBuilder->build(); } } @@ -111,12 +122,18 @@ public function processNode( return $ruleErrorBuilder->tip($tipText); }; - if ($nodeType->getValue() === false || $originalNode->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME) !== true) { - $errors[] = $addTip(RuleErrorBuilder::message(sprintf( + $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.'); + } + + $errors[] = $errorBuilder->build(); } } } diff --git a/src/Rules/Comparison/BooleanNotConstantConditionRule.php b/src/Rules/Comparison/BooleanNotConstantConditionRule.php index cf288498c1..0318446347 100644 --- a/src/Rules/Comparison/BooleanNotConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanNotConstantConditionRule.php @@ -19,6 +19,7 @@ class BooleanNotConstantConditionRule implements Rule public function __construct( private ConstantConditionRuleHelper $helper, private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, ) { } @@ -48,12 +49,18 @@ public function processNode( return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); }; - if ($exprType->getValue() === true || $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME) !== true) { + $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()); + if (!$exprType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Negated boolean expression is always %s.', - $exprType->getValue() ? 'false' : 'true', - )))->line($node->expr->getLine())->build(), + $errorBuilder->build(), ]; } } diff --git a/src/Rules/Comparison/BooleanOrConstantConditionRule.php b/src/Rules/Comparison/BooleanOrConstantConditionRule.php index 92bba0f7c7..73ffb29677 100644 --- a/src/Rules/Comparison/BooleanOrConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanOrConstantConditionRule.php @@ -22,6 +22,7 @@ public function __construct( private ConstantConditionRuleHelper $helper, private bool $treatPhpDocTypesAsCertain, private bool $bleedingEdge, + private bool $reportAlwaysTrueInLastCondition, ) { } @@ -55,12 +56,17 @@ public function processNode( return $ruleErrorBuilder->tip($tipText); }; - if ($leftType->getValue() === false || $originalNode->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME) !== true) { - $messages[] = $addTipLeft(RuleErrorBuilder::message(sprintf( + $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', - )))->line($originalNode->left->getLine())->build(); + )))->line($originalNode->left->getLine()); + if ($leftType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + $messages[] = $errorBuilder->build(); } } @@ -86,12 +92,17 @@ public function processNode( return $ruleErrorBuilder->tip($tipText); }; - if ($rightType->getValue() === false || $originalNode->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME) !== true) { - $messages[] = $addTipRight(RuleErrorBuilder::message(sprintf( + $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(); + )))->line($originalNode->right->getLine()); + if ($rightType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + $messages[] = $errorBuilder->build(); } } @@ -111,12 +122,18 @@ public function processNode( return $ruleErrorBuilder->tip($tipText); }; - if ($nodeType->getValue() === false || $originalNode->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME) !== true) { - $messages[] = $addTip(RuleErrorBuilder::message(sprintf( + $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.'); + } + + $messages[] = $errorBuilder->build(); } } } diff --git a/src/Rules/Comparison/ConstantConditionRuleHelper.php b/src/Rules/Comparison/ConstantConditionRuleHelper.php index b69eed4b15..60da2b56ff 100644 --- a/src/Rules/Comparison/ConstantConditionRuleHelper.php +++ b/src/Rules/Comparison/ConstantConditionRuleHelper.php @@ -6,7 +6,6 @@ use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; -use PHPStan\Parser\LastConditionVisitor; use PHPStan\Type\BooleanType; class ConstantConditionRuleHelper @@ -60,11 +59,6 @@ public function shouldSkip(Scope $scope, Expr $expr): bool return true; } - if ($expr->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME) === true) { - // always-true should not be reported because last condition - return true; - } - if ( $expr instanceof FuncCall || $expr instanceof MethodCall diff --git a/src/Rules/Comparison/ConstantLooseComparisonRule.php b/src/Rules/Comparison/ConstantLooseComparisonRule.php index e9b9e4b929..b60166ea65 100644 --- a/src/Rules/Comparison/ConstantLooseComparisonRule.php +++ b/src/Rules/Comparison/ConstantLooseComparisonRule.php @@ -17,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, + ) { } @@ -32,33 +36,50 @@ 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 []; } + $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->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + }; + 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 ? '==' : '!=', $scope->getType($node->left)->describe(VerbosityLevel::value()), $scope->getType($node->right)->describe(VerbosityLevel::value()), - ))->build(), + )))->build(), ]; } elseif ($this->checkAlwaysTrueLooseComparison) { - if ($node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME) === true) { + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { return []; } - return [ - RuleErrorBuilder::message(sprintf( - 'Loose comparison using %s between %s and %s will always evaluate to true.', - $node instanceof Node\Expr\BinaryOp\Equal ? '==' : '!=', - $scope->getType($node->left)->describe(VerbosityLevel::value()), - $scope->getType($node->right)->describe(VerbosityLevel::value()), - ))->build(), - ]; + $errorBuilder = $addTip(RuleErrorBuilder::message(sprintf( + 'Loose comparison using %s between %s and %s will always evaluate to true.', + $node instanceof Node\Expr\BinaryOp\Equal ? '==' : '!=', + $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.'); + } + + return [$errorBuilder->build()]; } return []; diff --git a/src/Rules/Comparison/ElseIfConstantConditionRule.php b/src/Rules/Comparison/ElseIfConstantConditionRule.php index 31df891540..5ce0b17ff1 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, ) { } @@ -46,8 +48,10 @@ public function processNode( 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 [ - $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()) @@ -56,9 +60,14 @@ public function processNode( 'depth' => $node->getAttribute('statementDepth'), 'order' => $node->getAttribute('statementOrder'), 'value' => $exprType->getValue(), - ]) - ->build(), - ]; + ]); + + if ($exprType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { + $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + } + + return [$errorBuilder->build()]; + } } return []; diff --git a/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php index 37ecddb527..cb7e2d6846 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php @@ -20,6 +20,7 @@ public function __construct( private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, private bool $checkAlwaysTrueCheckTypeFunctionCall, private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, ) { } @@ -66,17 +67,21 @@ public function processNode(Node $node, Scope $scope): array )))->build(), ]; } elseif ($this->checkAlwaysTrueCheckTypeFunctionCall) { - if ($node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME) === true) { + $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); + if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { return []; } - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Call to function %s()%s will always evaluate to true.', - $functionName, - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), - )))->build(), - ]; + $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.'); + } + + return [$errorBuilder->build()]; } return []; diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 7bfb5da33c..9463072340 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -59,6 +59,9 @@ 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); @@ -99,11 +102,12 @@ public function findSpecifiedType( $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 ($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; @@ -112,8 +116,9 @@ public function findSpecifiedType( return false; } } + + return null; } - return null; } if (!$haystackType instanceof ConstantArrayType || count($haystackType->getValueTypes()) > 0) { @@ -130,14 +135,14 @@ public function findSpecifiedType( continue; } - foreach (TypeUtils::getConstantScalars($haystackArrayValueType) as $constantScalarType) { + foreach ($haystackArrayValueType->getConstantScalarTypes() as $constantScalarType) { if ($constantScalarType->isSuperTypeOf($needleType)->yes()) { continue 3; } } } } else { - foreach (TypeUtils::getConstantScalars($haystackArrayType->getIterableValueType()) as $constantScalarType) { + foreach ($haystackArrayType->getIterableValueType()->getConstantScalarTypes() as $constantScalarType) { if ($constantScalarType->isSuperTypeOf($needleType)->yes()) { continue 2; } @@ -359,10 +364,14 @@ private function determineContext(Scope $scope, Expr $node): TypeSpecifierContex 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()); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $node->getArgs(), $functionReflection->getVariants(), $functionReflection->getNamedArgumentsVariants()); $returnType = TypeUtils::resolveLateResolvableTypes($parametersAcceptor->getReturnType()); return $returnType->isVoid()->yes() ? TypeSpecifierContext::createNull() : TypeSpecifierContext::createTruthy(); @@ -371,7 +380,7 @@ private function determineContext(Scope $scope, Expr $node): TypeSpecifierContex $methodCalledOnType = $scope->getType($node->var); $methodReflection = $scope->getMethodReflection($methodCalledOnType, $node->name->name); if ($methodReflection !== null) { - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $node->getArgs(), $methodReflection->getVariants()); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $node->getArgs(), $methodReflection->getVariants(), $methodReflection->getNamedArgumentsVariants()); $returnType = TypeUtils::resolveLateResolvableTypes($parametersAcceptor->getReturnType()); return $returnType->isVoid()->yes() ? TypeSpecifierContext::createNull() : TypeSpecifierContext::createTruthy(); @@ -385,7 +394,7 @@ private function determineContext(Scope $scope, Expr $node): TypeSpecifierContex $staticMethodReflection = $scope->getMethodReflection($calleeType, $node->name->name); if ($staticMethodReflection !== null) { - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $node->getArgs(), $staticMethodReflection->getVariants()); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $node->getArgs(), $staticMethodReflection->getVariants(), $staticMethodReflection->getNamedArgumentsVariants()); $returnType = TypeUtils::resolveLateResolvableTypes($parametersAcceptor->getReturnType()); return $returnType->isVoid()->yes() ? TypeSpecifierContext::createNull() : TypeSpecifierContext::createTruthy(); diff --git a/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php index 560d06e64d..9bf87802f7 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php @@ -22,6 +22,7 @@ public function __construct( private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, private bool $checkAlwaysTrueCheckTypeFunctionCall, private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, ) { } @@ -66,19 +67,23 @@ public function processNode(Node $node, Scope $scope): array )))->build(), ]; } elseif ($this->checkAlwaysTrueCheckTypeFunctionCall) { - if ($node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME) === true) { + $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.'); + } + + return [$errorBuilder->build()]; } return []; diff --git a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php index 0ae7054d3a..af14a9898d 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php @@ -22,6 +22,7 @@ public function __construct( private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, private bool $checkAlwaysTrueCheckTypeFunctionCall, private bool $treatPhpDocTypesAsCertain, + private bool $reportAlwaysTrueInLastCondition, ) { } @@ -67,20 +68,23 @@ public function processNode(Node $node, Scope $scope): array )))->build(), ]; } elseif ($this->checkAlwaysTrueCheckTypeFunctionCall) { - if ($node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME) === true) { + $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(), - ]; + return [$errorBuilder->build()]; } return []; diff --git a/src/Rules/Comparison/LogicalXorConstantConditionRule.php b/src/Rules/Comparison/LogicalXorConstantConditionRule.php new file mode 100644 index 0000000000..23116934e2 --- /dev/null +++ b/src/Rules/Comparison/LogicalXorConstantConditionRule.php @@ -0,0 +1,99 @@ + + */ +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); + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + if ($leftType instanceof ConstantBooleanType) { + $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $tipText, $node): RuleErrorBuilder { + if (!$this->treatPhpDocTypesAsCertain) { + return $ruleErrorBuilder; + } + + $booleanNativeType = $this->helper->getNativeBooleanType($scope, $node->left); + if ($booleanNativeType instanceof ConstantBooleanType) { + return $ruleErrorBuilder; + } + + return $ruleErrorBuilder->tip($tipText); + }; + + $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', + )))->line($node->left->getLine()); + 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, $tipText): RuleErrorBuilder { + if (!$this->treatPhpDocTypesAsCertain) { + return $ruleErrorBuilder; + } + + $booleanNativeType = $this->helper->getNativeBooleanType( + $scope, + $node->right, + ); + if ($booleanNativeType instanceof ConstantBooleanType) { + return $ruleErrorBuilder; + } + + return $ruleErrorBuilder->tip($tipText); + }; + + $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', + )))->line($node->right->getLine()); + 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 55cd6e74a6..913cd22314 100644 --- a/src/Rules/Comparison/MatchExpressionRule.php +++ b/src/Rules/Comparison/MatchExpressionRule.php @@ -30,6 +30,7 @@ public function __construct( private bool $checkAlwaysTrueStrictComparison, private bool $disableUnreachable, private bool $reportAlwaysTrueInLastCondition, + private bool $treatPhpDocTypesAsCertain, ) { } @@ -43,12 +44,16 @@ public function processNode(Node $node, Scope $scope): array { $matchCondition = $node->getCondition(); $matchConditionType = $scope->getType($matchCondition); - $nextArmIsDead = false; + $nextArmIsDeadForType = false; + $nextArmIsDeadForNativeType = false; $errors = []; $armsCount = count($node->getArms()); $hasDefault = false; foreach ($node->getArms() as $i => $arm) { - if ($nextArmIsDead) { + if ( + $nextArmIsDeadForNativeType + || ($nextArmIsDeadForType && $this->treatPhpDocTypesAsCertain) + ) { if (!$this->disableUnreachable) { $errors[] = RuleErrorBuilder::message('Match arm is unreachable because previous comparison is always true.')->line($arm->getLine())->build(); } @@ -64,13 +69,23 @@ public function processNode(Node $node, Scope $scope): array $matchCondition, $armCondition->getCondition(), ); + $armConditionResult = $armConditionScope->getType($armConditionExpr); if (!$armConditionResult instanceof ConstantBooleanType) { continue; } - if ($armConditionResult->getValue()) { - $nextArmIsDead = true; + $nextArmIsDeadForType = true; + } + + if (!$this->treatPhpDocTypesAsCertain) { + $armConditionNativeResult = $armConditionScope->getNativeType($armConditionExpr); + if (!$armConditionNativeResult instanceof ConstantBooleanType) { + continue; + } + if ($armConditionNativeResult->getValue()) { + $nextArmIsDeadForNativeType = true; + } } if ($matchConditionType instanceof ConstantBooleanType) { @@ -106,7 +121,7 @@ public function processNode(Node $node, Scope $scope): array } } - if (!$hasDefault && !$nextArmIsDead) { + if (!$hasDefault && !$nextArmIsDeadForType) { $remainingType = $node->getEndScope()->getType($matchCondition); $cases = $remainingType->getEnumCases(); $casesCount = count($cases); diff --git a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php index 838012b07b..e4162a85a8 100644 --- a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php +++ b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php @@ -44,14 +44,27 @@ public function processNode(Node $node, Scope $scope): array $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->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + }; + 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 ? '===' : '!==', $leftType->describe(VerbosityLevel::value()), $rightType->describe(VerbosityLevel::value()), - ))->build(), + )))->build(), ]; } elseif ($this->checkAlwaysTrueStrictComparison) { $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); @@ -59,14 +72,22 @@ public function processNode(Node $node, Scope $scope): array return []; } - $errorBuilder = RuleErrorBuilder::message(sprintf( + $errorBuilder = $addTip(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()), - )); + ))); if ($isLast === false && !$this->reportAlwaysTrueInLastCondition) { - $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); + $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.'); } return [ diff --git a/src/Rules/Comparison/UsageOfVoidMatchExpressionRule.php b/src/Rules/Comparison/UsageOfVoidMatchExpressionRule.php index 53f9f5a774..97744cec9e 100644 --- a/src/Rules/Comparison/UsageOfVoidMatchExpressionRule.php +++ b/src/Rules/Comparison/UsageOfVoidMatchExpressionRule.php @@ -21,7 +21,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { if (!$scope->isInFirstLevelStatement()) { - $matchResultType = $scope->getType($node); + $matchResultType = $scope->getKeepVoidType($node); if ($matchResultType->isVoid()->yes()) { return [RuleErrorBuilder::message('Result of match expression (void) is used.')->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/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/MagicConstantContextRule.php b/src/Rules/Constants/MagicConstantContextRule.php new file mode 100644 index 0000000000..f7cf06f4df --- /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()), + )->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()), + )->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()), + )->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()), + )->build(), + ]; + } + } + return []; + } + +} diff --git a/src/Rules/Constants/MissingClassConstantTypehintRule.php b/src/Rules/Constants/MissingClassConstantTypehintRule.php index 91a3679d3c..dca968ddeb 100644 --- a/src/Rules/Constants/MissingClassConstantTypehintRule.php +++ b/src/Rules/Constants/MissingClassConstantTypehintRule.php @@ -51,7 +51,10 @@ public function processNode(Node $node, Scope $scope): array 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) { 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..3a69334af7 100644 --- a/src/Rules/Constants/OverridingConstantRule.php +++ b/src/Rules/Constants/OverridingConstantRule.php @@ -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( @@ -98,6 +94,34 @@ private function processSingleConstant(ClassReflection $classReflection, string 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(), + ))->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()), + ))->nonIgnorable()->build(); + } + } + if (!$prototype->hasPhpDocType()) { return $errors; } diff --git a/src/Rules/Constants/ValueAssignedToClassConstantRule.php b/src/Rules/Constants/ValueAssignedToClassConstantRule.php new file mode 100644 index 0000000000..faee3beb80 --- /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 RuleError[] + */ + 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()->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()), + ))->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()), + ))->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)->build(), + ]; + } + +} diff --git a/src/Rules/DeadCode/NoopRule.php b/src/Rules/DeadCode/NoopRule.php index cd18fda0ca..9e9c389aa6 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 $logicalXor) { } @@ -36,18 +36,63 @@ 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->logicalXor) { + if ($expr instanceof Node\Expr\BinaryOp\LogicalXor) { + return [ + RuleErrorBuilder::message( + 'Unused result of "xor" operator.', + )->line($expr->getLine()) + ->tip('This operator has unexpected precedence, try disambiguating the logic with parentheses ().') + ->build(), + ]; + } + if ($expr instanceof Node\Expr\BinaryOp\LogicalAnd || $expr instanceof Node\Expr\BinaryOp\LogicalOr) { + if (!$this->isNoopExpr($expr->right)) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Unused result of "%s" operator.', + $expr->getOperatorSigil(), + ))->line($expr->getLine()) + ->tip('This operator has unexpected precedence, try disambiguating the logic with parentheses ().') + ->build(), + ]; + } + + if ($expr instanceof Node\Expr\BinaryOp\BooleanAnd || $expr instanceof Node\Expr\BinaryOp\BooleanOr) { + if (!$this->isNoopExpr($expr->right)) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Unused result of "%s" operator.', + $expr->getOperatorSigil(), + ))->line($expr->getLine()) + ->build(), + ]; + } + + if ($expr instanceof Node\Expr\Ternary) { + $if = $expr->if; + if ($if === null) { + $if = $expr->cond; + } + + if (!$this->isNoopExpr($if) || !$this->isNoopExpr($expr->else)) { + return []; + } + + return [ + RuleErrorBuilder::message('Unused result of ternary operator.') + ->line($expr->getLine()) + ->build(), + ]; + } + } + if (!$this->isNoopExpr($expr)) { return []; } @@ -65,4 +110,18 @@ public function processNode(Node $node, Scope $scope): array ]; } + 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/UnreachableStatementRule.php b/src/Rules/DeadCode/UnreachableStatementRule.php index 8f4793267a..ffe4876c6d 100644 --- a/src/Rules/DeadCode/UnreachableStatementRule.php +++ b/src/Rules/DeadCode/UnreachableStatementRule.php @@ -21,10 +21,6 @@ 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') diff --git a/src/Rules/DeadCode/UnusedPrivateConstantRule.php b/src/Rules/DeadCode/UnusedPrivateConstantRule.php index 991d4d8dc5..faf7672ef8 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\TrinaryLogic; +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,25 +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; + $constantReflection = $fetchScope->getConstantReflection($fetchedOnClass, $fetchNode->name->toString()); + if ($constantReflection === null) { + if (!$classType->isSuperTypeOf($fetchedOnClass)->no()) { + unset($constants[$fetchNode->name->toString()]); } - } else { - $classExprType = $fetch->getScope()->getType($fetchNode->class); - $isDifferentClass = TrinaryLogic::createNo()->lazyOr( - $classExprType->getObjectClassNames(), - static fn (string $objectClassName) => TrinaryLogic::createFromBoolean($objectClassName !== $classReflection->getName()), - ); - if ($isDifferentClass->yes()) { - continue; + continue; + } + + if ($constantReflection->getDeclaringClass()->getName() !== $classReflection->getName()) { + if (!$classType->isSuperTypeOf($fetchedOnClass)->no()) { + unset($constants[$fetchNode->name->toString()]); } + continue; } unset($constants[$fetchNode->name->toString()]); diff --git a/src/Rules/DeadCode/UnusedPrivateMethodRule.php b/src/Rules/DeadCode/UnusedPrivateMethodRule.php index e7b8660b8b..8f9b0ea5d1 100644 --- a/src/Rules/DeadCode/UnusedPrivateMethodRule.php +++ b/src/Rules/DeadCode/UnusedPrivateMethodRule.php @@ -7,12 +7,10 @@ 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 function array_map; use function count; @@ -25,6 +23,10 @@ class UnusedPrivateMethodRule implements Rule { + public function __construct(private AlwaysUsedMethodExtensionProvider $extensionProvider) + { + } + public function getNodeType(): string { return ClassMethodsNode::class; @@ -35,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) { @@ -60,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 = []; @@ -86,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)]); } } @@ -115,50 +131,57 @@ 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)) + $errors[] = RuleErrorBuilder::message(sprintf('%s %s::%s() is unused.', $methodType, $classReflection->getDisplayName(), $originalMethodName)) ->line($method->getNode()->getLine()) ->identifier('deadCode.unusedMethod') ->metadata([ 'classOrder' => $node->getClass()->getAttribute('statementOrder'), 'classDepth' => $node->getClass()->getAttribute('statementDepth'), 'classStartLine' => $node->getClass()->getStartLine(), - 'methodName' => $methodName, + 'methodName' => $originalMethodName, ]) ->build(); } diff --git a/src/Rules/DeadCode/UnusedPrivatePropertyRule.php b/src/Rules/DeadCode/UnusedPrivatePropertyRule.php index c5e12e547d..1c69a04348 100644 --- a/src/Rules/DeadCode/UnusedPrivatePropertyRule.php +++ b/src/Rules/DeadCode/UnusedPrivatePropertyRule.php @@ -9,15 +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 function array_key_exists; use function array_map; use function count; use function sprintf; -use function strpos; +use function str_contains; /** * @implements Rule @@ -48,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()) { @@ -68,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; } @@ -77,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; } @@ -134,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 { @@ -166,9 +175,9 @@ 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']) { diff --git a/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php b/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php index 4cbf0a7d92..611f06f31a 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; @@ -32,6 +39,20 @@ public function processNode(Node $node, Scope $scope): array ]; } + 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())), diff --git a/src/Rules/Exceptions/CaughtExceptionExistenceRule.php b/src/Rules/Exceptions/CaughtExceptionExistenceRule.php index bbd4b6be44..2cf04f7d01 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, ) { @@ -51,13 +51,12 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf('Caught class %s is not an exception.', $classReflection->getDisplayName()))->line($class->getLine())->build(); } - if (!$this->checkClassCaseSensitivity) { - continue; - } - $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/MissingCheckedExceptionInFunctionThrowsRule.php b/src/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRule.php index aa97cb9fdd..1f2b9ee8f9 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]) { diff --git a/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php b/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php index 40a398e25e..02e72dccb9 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]) { diff --git a/src/Rules/Exceptions/NoncapturingCatchRule.php b/src/Rules/Exceptions/NoncapturingCatchRule.php new file mode 100644 index 0000000000..c683705661 --- /dev/null +++ b/src/Rules/Exceptions/NoncapturingCatchRule.php @@ -0,0 +1,46 @@ + + */ +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() + ->build(), + ]; + } + +} diff --git a/src/Rules/Exceptions/ThrowExprTypeRule.php b/src/Rules/Exceptions/ThrowExprTypeRule.php new file mode 100644 index 0000000000..8d38531321 --- /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()), + ))->build(), + ]; + } + +} diff --git a/src/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRule.php b/src/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRule.php index 7fd2240ffd..896974469e 100644 --- a/src/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRule.php +++ b/src/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRule.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 PHPStan\TrinaryLogic; use PHPStan\Type\TypeUtils; use PHPStan\Type\VerbosityLevel; @@ -35,10 +33,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(); if ($functionReflection->getThrowType() === null || !$functionReflection->getThrowType()->isVoid()->yes()) { return []; diff --git a/src/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRule.php b/src/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRule.php index 9839b49ea1..0e6080eff9 100644 --- a/src/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRule.php +++ b/src/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRule.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\TrinaryLogic; use PHPStan\Type\TypeUtils; use PHPStan\Type\VerbosityLevel; @@ -35,10 +33,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(); if ($methodReflection->getThrowType() === null || !$methodReflection->getThrowType()->isVoid()->yes()) { return []; diff --git a/src/Rules/Exceptions/TooWideFunctionThrowTypeRule.php b/src/Rules/Exceptions/TooWideFunctionThrowTypeRule.php index fff550cd41..4c17bbb3be 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) { diff --git a/src/Rules/Exceptions/TooWideMethodThrowTypeRule.php b/src/Rules/Exceptions/TooWideMethodThrowTypeRule.php index 3a26b6843e..94bd6427ca 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(), 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/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index f7103af0b2..d2ea6b6173 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -26,6 +26,7 @@ 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; @@ -210,7 +211,7 @@ public function check( if ( !$funcCall instanceof Node\Expr\New_ && !$scope->isInFirstLevelStatement() - && $scope->getType($funcCall)->isVoid()->yes() + && $scope->getKeepVoidType($funcCall)->isVoid()->yes() ) { $errors[] = RuleErrorBuilder::message($messages[7])->line($funcCall->getLine())->build(); } @@ -252,19 +253,17 @@ public function check( if ($this->checkArgumentTypes) { $parameterType = TypeUtils::resolveLateResolvableTypes($parameter->getType()); - if (!$parameter->passedByReference()->createsNewVariable()) { + 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); - $parameterDescription = sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName()); $errors[] = RuleErrorBuilder::message(sprintf( $messages[6], - $argumentName === null ? sprintf( - '#%d %s', - $i + 1, - $parameterDescription, - ) : $parameterDescription, + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), $parameterType->describe($verbosityLevel), $argumentValueType->describe($verbosityLevel), ))->line($argumentLine)->acceptsReasonsTip($accepts->reasons)->build(); @@ -277,14 +276,9 @@ public function check( && !$this->unresolvableTypeHelper->containsUnresolvableType($originalParameter->getType()) && $this->unresolvableTypeHelper->containsUnresolvableType($parameterType) ) { - $parameterDescription = sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName()); $errors[] = RuleErrorBuilder::message(sprintf( $messages[13], - $argumentName === null ? sprintf( - '#%d %s', - $i + 1, - $parameterDescription, - ) : $parameterDescription, + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), ))->line($argumentLine)->build(); } } @@ -297,10 +291,9 @@ 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, + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), ))->line($argumentLine)->build(); continue; } @@ -324,10 +317,9 @@ 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(); } @@ -340,10 +332,9 @@ 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, + $this->describeParameter($parameter, $argumentName === null ? $i + 1 : null), ))->line($argumentLine)->build(); } @@ -523,4 +514,19 @@ private function processArguments( 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 9df74196e0..5a6b043629 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,10 +33,12 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; use PHPStan\Type\VerbosityLevel; +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; @@ -41,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, @@ -126,12 +132,12 @@ public function checkAnonymousFunction( 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) { + } else { $errors = array_merge( $errors, - $this->classCaseSensitivityCheck->checkClassNames([ + $this->classCheck->checkClassNames([ new ClassNameNodePair($class, $param->type), - ]), + ], $this->checkClassCaseSensitivity), ); } } @@ -164,12 +170,12 @@ public function checkAnonymousFunction( 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) { + } else { $errors = array_merge( $errors, - $this->classCaseSensitivityCheck->checkClassNames([ + $this->classCheck->checkClassNames([ new ClassNameNodePair($returnTypeClass, $returnTypeNode), - ]), + ], $this->checkClassCaseSensitivity), ); } } @@ -281,12 +287,13 @@ private function checkParametersAcceptor( ))->line($parameterNodeCallback()->getLine())->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; } @@ -311,12 +318,13 @@ private function checkParametersAcceptor( $errors[] = RuleErrorBuilder::message(sprintf($returnMessage, $class))->line($returnTypeNode->getLine())->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(); } @@ -364,6 +372,7 @@ private function checkRequiredParameterAfterOptional(array $parameterNodes): arr /** @var string|null $optionalParameter */ $optionalParameter = null; $errors = []; + $targetPhpVersion = null; foreach ($parameterNodes as $parameterNode) { if (!$parameterNode->var instanceof Variable) { throw new ShouldNotHappenException(); @@ -373,7 +382,15 @@ 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())->build(); + $targetPhpVersion = null; continue; } if ($parameterNode->default === null) { @@ -392,7 +409,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 c428ea78a6..5f6708945d 100644 --- a/src/Rules/FunctionReturnTypeCheck.php +++ b/src/Rules/FunctionReturnTypeCheck.php @@ -11,7 +11,6 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeUtils; use PHPStan\Type\VerbosityLevel; -use PHPStan\Type\VoidType; use function sprintf; class FunctionReturnTypeCheck @@ -53,7 +52,7 @@ public function checkReturnType( } } - $isVoidSuperType = (new VoidType())->isSuperTypeOf($returnType); + $isVoidSuperType = $returnType->isVoid(); $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($returnType, null); if ($returnValue === null) { if (!$isVoidSuperType->no()) { diff --git a/src/Rules/Functions/ArrayFilterRule.php b/src/Rules/Functions/ArrayFilterRule.php index c34dde0687..b60389d188 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 @@ -38,13 +39,28 @@ public function processNode(Node $node, Scope $scope): array return []; } - $functionName = $this->reflectionProvider->resolveFunctionName($node->name, $scope); + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + if ($functionReflection->getName() !== 'array_filter') { + return []; + } + + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $node->getArgs(), + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); - if ($functionName === null || strtolower($functionName) !== 'array_filter') { + $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); + if ($normalizedFuncCall === null) { return []; } - $args = $node->getArgs(); + $args = $normalizedFuncCall->getArgs(); if (count($args) !== 1) { return []; } @@ -57,11 +73,18 @@ public function processNode(Node $node, Scope $scope): array 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()), + )); + if ($this->treatPhpDocTypesAsCertain) { + $nativeArrayType = $scope->getNativeType($args[0]->value); + if (!$nativeArrayType->isIterableAtLeastOnce()->no()) { + $errorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + } + } return [ - RuleErrorBuilder::message(sprintf( - $message, - $arrayType->describe(VerbosityLevel::value()), - ))->build(), + $errorBuilder->build(), ]; } @@ -70,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()), + )); + + if ($this->treatPhpDocTypesAsCertain) { + $nativeArrayType = $scope->getNativeType($args[0]->value); + $isNativeSuperType = $falsyType->isSuperTypeOf($nativeArrayType->getIterableValueType()); + if (!$isNativeSuperType->no()) { + $errorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + } + } + 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()), + )); + + if ($this->treatPhpDocTypesAsCertain) { + $nativeArrayType = $scope->getNativeType($args[0]->value); + $isNativeSuperType = $falsyType->isSuperTypeOf($nativeArrayType->getIterableValueType()); + if (!$isNativeSuperType->yes()) { + $errorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + } + } + 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..78a5607532 --- /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()), + )); + if ($this->treatPhpDocTypesAsCertain) { + $nativeArrayType = $scope->getNativeType($args[0]->value); + if (!$nativeArrayType->isIterableAtLeastOnce()->no()) { + $errorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + } + } + + 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()), + )); + if ($this->treatPhpDocTypesAsCertain) { + $nativeArrayType = $scope->getNativeType($args[0]->value); + if (!$nativeArrayType->isList()->yes()) { + $errorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + } + } + + return [ + $errorBuilder->build(), + ]; + } + + return []; + } + +} 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..151a64671e 100644 --- a/src/Rules/Functions/CallCallablesRule.php +++ b/src/Rules/Functions/CallCallablesRule.php @@ -96,6 +96,7 @@ public function processNode( $scope, $node->getArgs(), $parametersAcceptors, + null, ); if ($type instanceof ClosureType) { diff --git a/src/Rules/Functions/CallToFunctionParametersRule.php b/src/Rules/Functions/CallToFunctionParametersRule.php index 342b79ffc1..d45a8a5e93 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(), 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/ExistingClassesInArrowFunctionTypehintsRule.php b/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php index ecaf404573..04c9bc7c3a 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,17 @@ 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.') + ->nonIgnorable() + ->build(); + } + } + + return array_merge($messages, $this->check->checkAnonymousFunction( $scope, $node->getParams(), $node->getReturnType(), @@ -33,7 +48,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/InvalidLexicalVariablesInClosureUseRule.php b/src/Rules/Functions/InvalidLexicalVariablesInClosureUseRule.php new file mode 100644 index 0000000000..8de23b7e22 --- /dev/null +++ b/src/Rules/Functions/InvalidLexicalVariablesInClosureUseRule.php @@ -0,0 +1,95 @@ + + */ +class InvalidLexicalVariablesInClosureUseRule implements Rule +{ + + private const SUPERGLOBAL_NAMES = [ + '_COOKIE', + '_ENV', + '_FILES', + '_GET', + '_POST', + '_REQUEST', + '_SERVER', + '_SESSION', + 'GLOBALS', + ]; + + 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->getLine()) + ->nonIgnorable() + ->build(); + continue; + } + + if (in_array($var, self::SUPERGLOBAL_NAMES, true)) { + $errors[] = RuleErrorBuilder::message(sprintf('Cannot use superglobal variable $%s as lexical variable.', $var)) + ->line($use->getLine()) + ->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->getLine()) + ->nonIgnorable() + ->build(); + } + + return $errors; + } + +} 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 10d2e8eb45..0be86d7529 100644 --- a/src/Rules/Functions/PrintfParametersRule.php +++ b/src/Rules/Functions/PrintfParametersRule.php @@ -108,7 +108,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'; @@ -116,7 +116,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); @@ -133,6 +133,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/RedefinedParametersRule.php b/src/Rules/Functions/RedefinedParametersRule.php new file mode 100644 index 0000000000..7824a1bf80 --- /dev/null +++ b/src/Rules/Functions/RedefinedParametersRule.php @@ -0,0 +1,57 @@ + + */ +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))->nonIgnorable()->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Functions/VariadicParametersDeclarationRule.php b/src/Rules/Functions/VariadicParametersDeclarationRule.php new file mode 100644 index 0000000000..e0741b2a2a --- /dev/null +++ b/src/Rules/Functions/VariadicParametersDeclarationRule.php @@ -0,0 +1,48 @@ + + */ +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()->build(); + } + + return $errors; + } + +} 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/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..ccce3e92e9 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 []; } diff --git a/src/Rules/Generics/FunctionSignatureVarianceRule.php b/src/Rules/Generics/FunctionSignatureVarianceRule.php index df54a8218e..9e5bea1705 100644 --- a/src/Rules/Generics/FunctionSignatureVarianceRule.php +++ b/src/Rules/Generics/FunctionSignatureVarianceRule.php @@ -33,6 +33,7 @@ public function processNode(Node $node, Scope $scope): array 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, 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 7660f86cb2..46ae4602ed 100644 --- a/src/Rules/Generics/GenericAncestorsCheck.php +++ b/src/Rules/Generics/GenericAncestorsCheck.php @@ -10,6 +10,7 @@ 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; @@ -52,6 +53,7 @@ public function check( string $notEnoughTypesMessage, string $extraTypesMessage, string $typeIsNotSubtypeMessage, + string $typeProjectionIsNotAllowedMessage, string $invalidTypeMessage, string $genericClassInNonGenericObjectType, string $invalidVarianceMessage, @@ -87,6 +89,8 @@ public function check( $notEnoughTypesMessage, $extraTypesMessage, $typeIsNotSubtypeMessage, + '', + '', ); $messages = array_merge($messages, $genericObjectTypeCheckMessages); @@ -106,6 +110,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) { diff --git a/src/Rules/Generics/GenericObjectTypeCheck.php b/src/Rules/Generics/GenericObjectTypeCheck.php index db2eaf27d6..46b73e2980 100644 --- a/src/Rules/Generics/GenericObjectTypeCheck.php +++ b/src/Rules/Generics/GenericObjectTypeCheck.php @@ -8,6 +8,9 @@ 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; @@ -29,6 +32,8 @@ public function check( string $notEnoughTypesMessage, string $extraTypesMessage, string $typeIsNotSubtypeMessage, + string $typeProjectionHasConflictingVarianceMessage, + string $typeProjectionIsRedundantMessage, ): array { $genericTypes = $this->getGenericTypes($phpDocType); @@ -55,6 +60,7 @@ public function check( $templateTypes = array_values($classReflection->getTemplateTypeMap()->getTypes()); $genericTypeTypes = $genericType->getTypes(); + $genericTypeVariances = $genericType->getVariances(); $templateTypesCount = count($templateTypes); $genericTypeTypesCount = count($genericTypeTypes); if ($templateTypesCount > $genericTypeTypesCount) { @@ -84,8 +90,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 +130,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()), 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 c0a2b0f372..02caaff9c1 100644 --- a/src/Rules/Generics/MethodSignatureVarianceRule.php +++ b/src/Rules/Generics/MethodSignatureVarianceRule.php @@ -32,10 +32,11 @@ public function processNode(Node $node, Scope $scope): array 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->isPrivate(), + $method->isStatic(), + $method->isPrivate() || $method->getName() === '__construct', ); } diff --git a/src/Rules/Generics/MethodTagTemplateTypeRule.php b/src/Rules/Generics/MethodTagTemplateTypeRule.php new file mode 100644 index 0000000000..58417e8c6a --- /dev/null +++ b/src/Rules/Generics/MethodTagTemplateTypeRule.php @@ -0,0 +1,84 @@ + + */ +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)))->build(); + } + } + + return $messages; + } + +} diff --git a/src/Rules/Generics/MethodTemplateTypeRule.php b/src/Rules/Generics/MethodTemplateTypeRule.php index 7f51d9e0d5..bbfca3a209 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, 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..72a0a16429 100644 --- a/src/Rules/Generics/TemplateTypeCheck.php +++ b/src/Rules/Generics/TemplateTypeCheck.php @@ -3,10 +3,11 @@ 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\RuleErrorBuilder; @@ -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, @@ -52,6 +54,7 @@ public function __construct( * @return RuleError[] */ public function check( + Scope $scope, Node $node, TemplateTypeScope $templateTypeScope, array $templateTags, @@ -63,7 +66,7 @@ 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, @@ -92,12 +95,9 @@ public function check( ))->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,6 +111,7 @@ public function check( && $boundTypeClass !== BooleanType::class && $boundTypeClass !== ObjectWithoutClassType::class && $boundTypeClass !== ObjectType::class + && $boundTypeClass !== ObjectShapeType::class && $boundTypeClass !== GenericObjectType::class && $boundTypeClass !== KeyOfType::class && !$boundType instanceof UnionType @@ -127,6 +128,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..3018eb43c4 100644 --- a/src/Rules/Generics/UsedTraitsRule.php +++ b/src/Rules/Generics/UsedTraitsRule.php @@ -73,6 +73,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 3d1670a3c8..432a47af4a 100644 --- a/src/Rules/Generics/VarianceCheck.php +++ b/src/Rules/Generics/VarianceCheck.php @@ -2,7 +2,7 @@ namespace PHPStan\Rules\Generics; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Generic\TemplateType; @@ -13,10 +13,18 @@ class VarianceCheck { + public function __construct( + private bool $checkParamOutVariance, + private bool $strictStaticVariance, + ) + { + } + /** @return RuleError[] */ public function checkParametersAcceptor( - ParametersAcceptor $parametersAcceptor, + ParametersAcceptorWithPhpDocs $parametersAcceptor, string $parameterTypeMessage, + string $parameterOutTypeMessage, string $returnTypeMessage, string $generalMessage, bool $isStatic, @@ -44,20 +52,35 @@ public function checkParametersAcceptor( return $errors; } + $covariant = TemplateTypeVariance::createCovariant(); + $parameterVariance = $isStatic && !$this->strictStaticVariance + ? TemplateTypeVariance::createStatic() + : TemplateTypeVariance::createContravariant(); + 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) { + 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; } 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/IssetCheck.php b/src/Rules/IssetCheck.php index f5389ad7f8..39f8a41f4f 100644 --- a/src/Rules/IssetCheck.php +++ b/src/Rules/IssetCheck.php @@ -8,6 +8,7 @@ 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; @@ -44,11 +45,14 @@ 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, + ); + } } return RuleErrorBuilder::message(sprintf('Variable $%s %s is never defined.', $expr->name, $operatorDescription))->build(); @@ -59,14 +63,14 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, cal $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 $error ?? $this->checkUndefined($expr->var, $scope, $operatorDescription); } + $dimType = $this->treatPhpDocTypesAsCertain + ? $scope->getType($expr->dim) + : $scope->getNativeType($expr->dim); + $hasOffsetValue = $type->hasOffsetValueType($dimType); if ($hasOffsetValue->no()) { if (!$this->checkAdvancedIsset) { return null; @@ -82,7 +86,7 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, cal )->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->hasExpressionType($expr)->yes()) { if (!$this->checkAdvancedIsset) { @@ -147,7 +151,7 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, cal } } - $propertyDescription = $this->propertyDescriptor->describeProperty($propertyReflection, $expr); + $propertyDescription = $this->propertyDescriptor->describeProperty($propertyReflection, $scope, $expr); $propertyType = $propertyReflection->getWritableType(); if ($error !== null) { return $error; @@ -191,7 +195,7 @@ 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); if ($error !== null) { return $error; } @@ -223,8 +227,8 @@ private function checkUndefined(Expr $expr, Scope $scope, string $operatorDescri } 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); diff --git a/src/Rules/Keywords/DeclareStrictTypesRule.php b/src/Rules/Keywords/DeclareStrictTypesRule.php new file mode 100644 index 0000000000..36610c7afa --- /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), + ), + ))->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.', + ))->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..13b94ebdbb 100644 --- a/src/Rules/Methods/AbstractMethodInNonAbstractClassRule.php +++ b/src/Rules/Methods/AbstractMethodInNonAbstractClassRule.php @@ -27,17 +27,29 @@ public function processNode(Node $node, Scope $scope): array } $class = $scope->getClassReflection(); - if ($class->isAbstract()) { - return []; + + if (!$class->isAbstract() && $node->isAbstract()) { + return [ + RuleErrorBuilder::message(sprintf( + '%s %s contains abstract method %s().', + $class->isInterface() ? 'Interface' : 'Non-abstract class', + $class->getDisplayName(), + $node->name->toString(), + ))->nonIgnorable()->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()->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..de779287e7 --- /dev/null +++ b/src/Rules/Methods/AbstractPrivateMethodRule.php @@ -0,0 +1,55 @@ + */ +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(), + ))->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(), diff --git a/src/Rules/Methods/CallStaticMethodsRule.php b/src/Rules/Methods/CallStaticMethodsRule.php index 7534017147..c55e61754d 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(), diff --git a/src/Rules/Methods/ConsistentConstructorRule.php b/src/Rules/Methods/ConsistentConstructorRule.php index 82affcbac6..e32d6fa551 100644 --- a/src/Rules/Methods/ConsistentConstructorRule.php +++ b/src/Rules/Methods/ConsistentConstructorRule.php @@ -5,10 +5,7 @@ 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\PhpMethodReflection; use PHPStan\Rules\Rule; use function strtolower; @@ -43,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/IllegalConstructorStaticCallRule.php b/src/Rules/Methods/IllegalConstructorStaticCallRule.php index ac3e0fa52a..b24aa87b66 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; /** @@ -37,16 +39,19 @@ public function processNode(Node $node, Scope $scope): array ]; } - 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 +65,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 0bdfecd6b2..f2c3940766 100644 --- a/src/Rules/Methods/IncompatibleDefaultParameterTypeRule.php +++ b/src/Rules/Methods/IncompatibleDefaultParameterTypeRule.php @@ -43,7 +43,8 @@ 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); $accepts = $parameterType->acceptsWithReason($defaultValueType, true); 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 67b410e392..491165ba4a 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\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{RuleError[], ExtendedMethodReflection|null} */ public function check( Scope $scope, @@ -50,13 +51,18 @@ public function check( if ($type instanceof ErrorType) { return [$typeResult->getUnknownClassErrors(), null]; } + + $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()), + $typeForDescribe->describe(VerbosityLevel::typeOnly()), ))->build(), ], null, @@ -105,7 +111,7 @@ public function check( [ RuleErrorBuilder::message(sprintf( 'Call to an undefined method %s::%s().', - $type->describe(VerbosityLevel::typeOnly()), + $typeForDescribe->describe(VerbosityLevel::typeOnly()), $methodName, ))->build(), ], diff --git a/src/Rules/Methods/MethodParameterComparisonHelper.php b/src/Rules/Methods/MethodParameterComparisonHelper.php index 097f64f8a7..c1ddaac674 100644 --- a/src/Rules/Methods/MethodParameterComparisonHelper.php +++ b/src/Rules/Methods/MethodParameterComparisonHelper.php @@ -3,8 +3,8 @@ 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; @@ -24,14 +24,14 @@ class MethodParameterComparisonHelper { - public function __construct(private PhpVersion $phpVersion) + public function __construct(private PhpVersion $phpVersion, private bool $genericPrototypeMessage) { } /** * @return RuleError[] */ - 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 */ $messages = []; @@ -47,7 +47,7 @@ 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(), @@ -73,7 +73,7 @@ public function compare(MethodPrototypeReflection $prototype, PhpMethodFromParse $method->getName(), $i + 1, $prototypeParameter->getName(), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), $prototype->getName(), )); @@ -92,7 +92,7 @@ public function compare(MethodPrototypeReflection $prototype, PhpMethodFromParse $method->getName(), $i + 1, $prototypeParameter->getName(), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), $prototype->getName(), )); @@ -133,7 +133,7 @@ public function compare(MethodPrototypeReflection $prototype, PhpMethodFromParse $method->getName(), $i + 1, $prototypeParameter->getName(), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), $prototype->getName(), )); @@ -164,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; } @@ -181,7 +178,7 @@ 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(), )); @@ -201,7 +198,7 @@ public function compare(MethodPrototypeReflection $prototype, PhpMethodFromParse $method->getName(), $i + 1, $prototypeParameter->getName(), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), $prototype->getName(), )); @@ -223,7 +220,7 @@ public function compare(MethodPrototypeReflection $prototype, PhpMethodFromParse $method->getName(), $i + 1, $prototypeParameter->getName(), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), $prototype->getName(), )); @@ -236,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)) { @@ -253,7 +246,7 @@ public function compare(MethodPrototypeReflection $prototype, PhpMethodFromParse $i + 1, $prototypeParameter->getName(), $prototypeParameterType->describe(VerbosityLevel::typeOnly()), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), $prototype->getName(), )); @@ -281,7 +274,7 @@ public function compare(MethodPrototypeReflection $prototype, PhpMethodFromParse $i + 1, $prototypeParameter->getName(), $prototypeParameterType->describe(VerbosityLevel::typeOnly()), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), $prototype->getName(), )); @@ -301,7 +294,7 @@ public function compare(MethodPrototypeReflection $prototype, PhpMethodFromParse $i + 1, $prototypeParameter->getName(), $prototypeParameterType->describe(VerbosityLevel::typeOnly()), - $prototype->getDeclaringClass()->getDisplayName(), + $prototypeDeclaringClass->getDisplayName($this->genericPrototypeMessage), $prototype->getName(), )); diff --git a/src/Rules/Methods/MethodSignatureRule.php b/src/Rules/Methods/MethodSignatureRule.php index 7c5126a821..dda585bf52 100644 --- a/src/Rules/Methods/MethodSignatureRule.php +++ b/src/Rules/Methods/MethodSignatureRule.php @@ -10,6 +10,8 @@ use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; +use PHPStan\Reflection\Php\NativeBuiltinMethodReflection; +use PHPStan\Reflection\Php\PhpClassReflectionExtension; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\TrinaryLogic; @@ -32,8 +34,10 @@ class MethodSignatureRule implements Rule { public function __construct( + private PhpClassReflectionExtension $phpClassReflectionExtension, private bool $reportMaybes, private bool $reportStatic, + private bool $abstractTraitMethod, ) { } @@ -60,7 +64,7 @@ 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; @@ -75,7 +79,7 @@ public function processNode(Node $node, Scope $scope): array $method->getName(), $returnTypeCompatibility->no() ? 'compatible' : 'covariant', $parentReturnType->describe(VerbosityLevel::value()), - $parentMethod->getDeclaringClass()->getDisplayName(), + $parentMethodDeclaringClass->getDisplayName(), $parentMethod->getName(), ))->build(); } @@ -100,7 +104,7 @@ 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(); } @@ -110,7 +114,7 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return ExtendedMethodReflection[] + * @return list */ private function collectParentMethods(string $methodName, ClassReflection $class): array { @@ -120,7 +124,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()]; } } @@ -129,7 +133,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; diff --git a/src/Rules/Methods/MethodVisibilityInInterfaceRule.php b/src/Rules/Methods/MethodVisibilityInInterfaceRule.php new file mode 100644 index 0000000000..f660aa2152 --- /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(), + ))->nonIgnorable()->build(), + ]; + } + +} diff --git a/src/Rules/Methods/MissingMethodReturnTypehintRule.php b/src/Rules/Methods/MissingMethodReturnTypehintRule.php index 0e56ba32f1..0831e51719 100644 --- a/src/Rules/Methods/MissingMethodReturnTypehintRule.php +++ b/src/Rules/Methods/MissingMethodReturnTypehintRule.php @@ -32,6 +32,13 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $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()) { diff --git a/src/Rules/Methods/NullsafeMethodCallRule.php b/src/Rules/Methods/NullsafeMethodCallRule.php index d060485df7..c7461cfbfc 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,13 +22,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 []; } diff --git a/src/Rules/Methods/OverridingMethodRule.php b/src/Rules/Methods/OverridingMethodRule.php index 0108a4d69b..686d04918f 100644 --- a/src/Rules/Methods/OverridingMethodRule.php +++ b/src/Rules/Methods/OverridingMethodRule.php @@ -6,15 +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\NativeBuiltinMethodReflection; +use PHPStan\Reflection\Php\PhpClassReflectionExtension; +use PHPStan\Reflection\Php\PhpMethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; +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; @@ -29,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, ) { } @@ -41,42 +53,82 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $method = $node->getMethodReflection(); - $prototype = $method->getPrototype(); - if ($prototype->getDeclaringClass()->getName() === $method->getDeclaringClass()->getName()) { + $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()->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(), + ))->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()->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(), + ))->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()->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(), + ))->build(); } if ($prototype->isStatic()) { @@ -85,7 +137,7 @@ 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(); } @@ -94,30 +146,32 @@ public function processNode(Node $node, Scope $scope): array '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(); } - 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()->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(); } - } 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(); @@ -130,34 +184,61 @@ 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(), + $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()->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 (!$this->methodParameterComparisonHelper->isReturnTypeCompatible($prototypeReturnType, $methodReturnType, $this->phpVersion->supportsReturnCovariance())) { + if ($reportReturnType && $prototype->isInternal()->yes()) { + $reportReturnType = !$this->hasReturnTypeWillChangeAttribute($node->getOriginalNode()); + } + } + } + + 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().', @@ -165,7 +246,7 @@ 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(); } else { @@ -175,7 +256,7 @@ 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(); } @@ -218,4 +299,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/StaticMethodCallCheck.php b/src/Rules/Methods/StaticMethodCallCheck.php index 65f1f3bfda..a77ccbce7a 100644 --- a/src/Rules/Methods/StaticMethodCallCheck.php +++ b/src/Rules/Methods/StaticMethodCallCheck.php @@ -8,11 +8,12 @@ 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\RuleErrorBuilder; @@ -21,8 +22,8 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\StaticType; use PHPStan\Type\StringType; -use PHPStan\Type\ThisType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; @@ -37,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, ) @@ -46,7 +47,7 @@ public function __construct( /** * @param Name|Expr $class - * @return array{RuleError[], MethodReflection|null} + * @return array{RuleError[], ExtendedMethodReflection|null} */ public function check( Scope $scope, @@ -128,10 +129,10 @@ public function check( ], null, ]; - } else { - $errors = $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($className, $class)]); } + $errors = $this->classCheck->checkClassNames([new ClassNameNodePair($className, $class)]); + $classType = $scope->resolveTypeByName($class); } @@ -140,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 { @@ -165,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()); diff --git a/src/Rules/MissingTypehintCheck.php b/src/Rules/MissingTypehintCheck.php index c8e6f27656..93cca1e458 100644 --- a/src/Rules/MissingTypehintCheck.php +++ b/src/Rules/MissingTypehintCheck.php @@ -85,11 +85,9 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array if ($iterableValue instanceof MixedType && !$iterableValue->isExplicitMixed()) { $iterablesWithMissingValueTypehint[] = $type; } - if (!$type instanceof IntersectionType) { - return $traverse($type); + if ($type instanceof IntersectionType && !$type->isList()->yes()) { + return $type; } - - return $type; } return $traverse($type); }); diff --git a/src/Rules/Names/UsedNamesRule.php b/src/Rules/Names/UsedNamesRule.php new file mode 100644 index 0000000000..4c0bb14f90 --- /dev/null +++ b/src/Rules/Names/UsedNamesRule.php @@ -0,0 +1,148 @@ + + */ +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 RuleError[] + */ + 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(), + )) + ->line($node->getLine()) + ->nonIgnorable() + ->build(), + ]; + } + $usedNames[$lowerNamespace][] = $name; + return []; + } + + return []; + } + + /** + * @param UseUse[] $uses + * @param array $usedNames + * @return RuleError[] + */ + 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(), + )) + ->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..94814fb370 100644 --- a/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php +++ b/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php @@ -6,7 +6,7 @@ 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\Rule; use PHPStan\Rules\RuleError; @@ -24,7 +24,7 @@ class ExistingNamesInGroupUseRule implements Rule public function __construct( private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private bool $checkFunctionNameCase, ) { @@ -105,7 +105,7 @@ private function checkFunction(Node\Name $name): ?RuleError private function checkClass(Node\Name $name): ?RuleError { - $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..0a3e679ed9 100644 --- a/src/Rules/Namespaces/ExistingNamesInUseRule.php +++ b/src/Rules/Namespaces/ExistingNamesInUseRule.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\RuleError; @@ -23,7 +23,7 @@ class ExistingNamesInUseRule implements Rule public function __construct( private ReflectionProvider $reflectionProvider, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, private bool $checkFunctionNameCase, ) { @@ -111,7 +111,7 @@ private function checkFunctions(array $uses): array */ 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/PhpDoc/AssertRuleHelper.php b/src/Rules/PhpDoc/AssertRuleHelper.php index 6acb4919a8..2bc59e5306 100644 --- a/src/Rules/PhpDoc/AssertRuleHelper.php +++ b/src/Rules/PhpDoc/AssertRuleHelper.php @@ -49,6 +49,10 @@ public function check(ExtendedMethodReflection|FunctionReflection $reflection, P continue; } + if (!$assert->isExplicit()) { + continue; + } + $assertedExpr = $assert->getParameter()->getExpr(new TypeExpr($parametersByName[$parameterName])); $assertedExprType = $this->initializerExprTypeResolver->getType($assertedExpr, $context); if ($assertedExprType instanceof ErrorType) { diff --git a/src/Rules/PhpDoc/ConditionalReturnTypeRuleHelper.php b/src/Rules/PhpDoc/ConditionalReturnTypeRuleHelper.php index 0c36ebc197..deceb4cc58 100644 --- a/src/Rules/PhpDoc/ConditionalReturnTypeRuleHelper.php +++ b/src/Rules/PhpDoc/ConditionalReturnTypeRuleHelper.php @@ -2,7 +2,7 @@ namespace PHPStan\Rules\PhpDoc; -use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ConditionalType; @@ -13,6 +13,7 @@ use PHPStan\Type\TypeTraverser; use PHPStan\Type\VerbosityLevel; use function array_key_exists; +use function count; use function sprintf; use function substr; @@ -22,10 +23,8 @@ class ConditionalReturnTypeRuleHelper /** * @return RuleError[] */ - public function check(ParametersAcceptor $acceptor): array + public function check(ParametersAcceptorWithPhpDocs $acceptor): array { - $templateTypeMap = $acceptor->getTemplateTypeMap(); - $conditionalTypes = []; $parametersByName = []; foreach ($acceptor->getParameters() as $parameter) { @@ -37,6 +36,16 @@ public function check(ParametersAcceptor $acceptor): array 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); + }); + } + $parametersByName[$parameter->getName()] = $parameter; } @@ -55,7 +64,17 @@ public function check(ParametersAcceptor $acceptor): array if ($subjectType instanceof StaticType) { continue; } - if (!$subjectType instanceof TemplateType || $templateTypeMap->getType($subjectType->getName()) === null) { + $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())))->build(); continue; } diff --git a/src/Rules/PhpDoc/GenericCallableRuleHelper.php b/src/Rules/PhpDoc/GenericCallableRuleHelper.php new file mode 100644 index 0000000000..5b6dfb115d --- /dev/null +++ b/src/Rules/PhpDoc/GenericCallableRuleHelper.php @@ -0,0 +1,117 @@ + $functionTemplateTags + * + * @return array + */ + 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, + ))->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, + ))->build(); + } + } + + return $traverse($type); + }); + + return $errors; + } + +} diff --git a/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php b/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php index cd96f6e8a0..05d2b847b5 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\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,10 +41,15 @@ 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; @@ -55,19 +58,14 @@ public function processNode(Node $node, Scope $scope): array /** * @return RuleError[] */ - 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) @@ -77,26 +75,24 @@ private function processSingleConstant(ClassReflection $classReflection, string $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); + } 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()), + $phpDocType->describe(VerbosityLevel::typeOnly()), + $nativeType->describe(VerbosityLevel::typeOnly()), ))->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()), + $phpDocType->describe(VerbosityLevel::typeOnly()), + $nativeType->describe(VerbosityLevel::typeOnly()), ))->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 ebfdba824f..aef16c24a3 100644 --- a/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php +++ b/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php @@ -31,6 +31,7 @@ public function __construct( private FileTypeMapper $fileTypeMapper, private GenericObjectTypeCheck $genericObjectTypeCheck, private UnresolvableTypeHelper $unresolvableTypeHelper, + private GenericCallableRuleHelper $genericCallableRuleHelper, ) { } @@ -42,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_) { @@ -55,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, @@ -63,7 +64,6 @@ 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 = []; @@ -126,6 +126,26 @@ public function processNode(Node $node, Scope $scope): array $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, + ), + )); + + $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 ($phpDocParamTag instanceof ParamOutTag) { @@ -176,6 +196,7 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message('PHPDoc tag @return contains unresolvable type.')->build(); } else { + $nativeReturnType = $this->getNativeReturnType($node, $scope); $isReturnSuperType = $nativeReturnType->isSuperTypeOf($phpDocReturnType); $errors = array_merge($errors, $this->genericObjectTypeCheck->check( $phpDocReturnType, @@ -183,6 +204,8 @@ 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( @@ -203,6 +226,16 @@ public function processNode(Node $node, Scope $scope): array $errors[] = $errorBuilder->build(); } + + $errors = array_merge($errors, $this->genericCallableRuleHelper->check( + $node, + $scope, + '@return', + $phpDocReturnType, + $functionName, + $resolvedPhpDoc->getTemplateTags(), + $scope->isInClass() ? $scope->getClassReflection() : null, + )); } } diff --git a/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php b/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php index e3929366c5..9136769c42 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,18 +57,18 @@ 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(); } - $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()), @@ -79,7 +78,7 @@ public function processNode(Node $node, Scope $scope): array $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()), @@ -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/InvalidPHPStanDocTagRule.php b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php index 3a40fa347b..27f312c3fd 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 @@ -51,9 +52,15 @@ class InvalidPHPStanDocTagRule implements Rule '@phpstan-allow-private-mutation', '@phpstan-readonly', '@phpstan-readonly-allow-private-mutation', + '@phpstan-require-extends', + '@phpstan-require-implements', ]; - public function __construct(private Lexer $phpDocLexer, private PhpDocParser $phpDocParser) + public function __construct( + private Lexer $phpDocLexer, + private PhpDocParser $phpDocParser, + private bool $checkAllInvalidPhpDocs, + ) { } @@ -64,17 +71,32 @@ 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 []; + } } + // todo $docComment = $node->getDocComment(); if ($docComment === null) { return []; @@ -85,7 +107,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; @@ -94,7 +116,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)) + ->build(); } return $errors; diff --git a/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php b/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php index bcf90bec7e..d17efa9b15 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,18 +39,32 @@ 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 []; + } } + // todo $docComment = $node->getDocComment(); if ($docComment === null) { return []; @@ -53,11 +76,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, '@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)) + ->build(); + + continue; + } elseif (!($phpDocTag->value instanceof InvalidTagValueNode)) { continue; } @@ -65,11 +103,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)) + ->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..4e518a0177 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, @@ -101,6 +101,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]) { @@ -124,19 +126,22 @@ public function processNode(Node $node, Scope $scope): array continue; } + if ($scope->isInClassExists($referencedClass)) { + continue; + } + $errors[] = RuleErrorBuilder::message(sprintf( sprintf('%s contains unknown class %%s.', $identifier), $referencedClass, ))->discoveringSymbolsTip()->build(); } - if (!$this->checkClassCaseSensitivity) { - continue; - } - $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..8a5bc9d823 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 []; } @@ -74,4 +75,32 @@ public function processNode(Node $node, Scope $scope): array ]; } + 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/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..d3739d5299 --- /dev/null +++ b/src/Rules/PhpDoc/RequireExtendsCheck.php @@ -0,0 +1,71 @@ + $extendsTags + * @return RuleError[] + */ + 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.'))->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())))->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()->build(); + continue; + } + + if (!$referencedClassReflection->isClass()) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends cannot contain non-class type %s.', $class))->build(); + } elseif ($referencedClassReflection->isFinal()) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends cannot contain final class %s.', $class))->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..c4a463f093 --- /dev/null +++ b/src/Rules/PhpDoc/RequireExtendsDefinitionClassRule.php @@ -0,0 +1,47 @@ + + */ +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.')->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..d4325e65c1 --- /dev/null +++ b/src/Rules/PhpDoc/RequireImplementsDefinitionClassRule.php @@ -0,0 +1,37 @@ + + */ +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.')->build(), + ]; + } + +} diff --git a/src/Rules/PhpDoc/RequireImplementsDefinitionTraitRule.php b/src/Rules/PhpDoc/RequireImplementsDefinitionTraitRule.php new file mode 100644 index 0000000000..e109385ca3 --- /dev/null +++ b/src/Rules/PhpDoc/RequireImplementsDefinitionTraitRule.php @@ -0,0 +1,78 @@ + + */ +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())))->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()->build(); + continue; + } + + if (!$referencedClassReflection->isInterface()) { + $errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-implements cannot contain non-interface type %s.', $class))->build(); + } else { + $errors = array_merge( + $errors, + $this->classCheck->checkClassNames([ + new ClassNameNodePair($class, $node), + ], $this->checkClassCaseSensitivity), + ); + } + } + + return $errors; + } + +} diff --git a/src/Rules/PhpDoc/VarTagTypeRuleHelper.php b/src/Rules/PhpDoc/VarTagTypeRuleHelper.php index 4698ae7097..6797ac244f 100644 --- a/src/Rules/PhpDoc/VarTagTypeRuleHelper.php +++ b/src/Rules/PhpDoc/VarTagTypeRuleHelper.php @@ -9,10 +9,12 @@ use PHPStan\PhpDoc\Tag\VarTag; use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\ConstantType; +use PHPStan\Type\ArrayType; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use PHPStan\Type\TypeUtils; use PHPStan\Type\VerbosityLevel; use function array_key_exists; use function count; @@ -22,7 +24,7 @@ class VarTagTypeRuleHelper { - public function __construct(private bool $checkTypeAgainstPhpDocType) + public function __construct(private bool $checkTypeAgainstPhpDocType, private bool $strictWideningCheck) { } @@ -73,6 +75,7 @@ public function checkExprType(Scope $scope, Node\Expr $expr, Type $varTagType): { $errors = []; $exprNativeType = $scope->getNativeType($expr); + $containsPhpStanType = $this->containsPhpStanType($varTagType); if ($this->shouldVarTagTypeBeReported($expr, $exprNativeType, $varTagType)) { $verbosity = VerbosityLevel::getRecommendedLevelByType($exprNativeType, $varTagType); $errors[] = RuleErrorBuilder::message(sprintf( @@ -80,9 +83,12 @@ public function checkExprType(Scope $scope, Node\Expr $expr, Type $varTagType): $varTagType->describe($verbosity), $exprNativeType->describe($verbosity), ))->build(); - } elseif ($this->checkTypeAgainstPhpDocType) { + } else { $exprType = $scope->getType($expr); - if ($this->shouldVarTagTypeBeReported($expr, $exprType, $varTagType)) { + 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.', @@ -92,11 +98,53 @@ public function checkExprType(Scope $scope, Node\Expr $expr, Type $varTagType): } } + 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), + ))->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()); @@ -106,10 +154,17 @@ private function shouldVarTagTypeBeReported(Node\Expr $expr, Type $type, Type $v return $this->checkType($type, $varTagType); } - private function checkType(Type $type, Type $varTagType): bool + private function checkType(Type $type, Type $varTagType, int $depth = 0): bool { - if ($type instanceof ConstantType) { - return $type->isSuperTypeOf($varTagType)->no(); + 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()) { @@ -124,7 +179,11 @@ private function checkType(Type $type, Type $varTagType): bool return !$innerType->isSuperTypeOf($innerVarTagType)->yes(); } - return $this->checkType($innerType, $innerVarTagType); + 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/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/NotAnalysedTraitRule.php b/src/Rules/Playground/NotAnalysedTraitRule.php new file mode 100644 index 0000000000..bf5da0aa79 --- /dev/null +++ b/src/Rules/Playground/NotAnalysedTraitRule.php @@ -0,0 +1,61 @@ + + */ +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, + )) + ->file($file) + ->line($line) + ->tip('See: https://phpstan.org/blog/how-phpstan-analyses-traits') + ->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Properties/AccessPropertiesRule.php b/src/Rules/Properties/AccessPropertiesRule.php index 825d815447..5681416c04 100644 --- a/src/Rules/Properties/AccessPropertiesRule.php +++ b/src/Rules/Properties/AccessPropertiesRule.php @@ -15,6 +15,7 @@ use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ErrorType; +use PHPStan\Type\StaticType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function array_map; @@ -78,12 +79,17 @@ 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()), + $typeForDescribe->describe(VerbosityLevel::typeOnly()), ))->build(), ]; } @@ -138,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, )); if ($typeResult->getTip() !== null) { $ruleErrorBuilder->tip($typeResult->getTip()); + } else { + $ruleErrorBuilder->tip('Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property'); } return [ diff --git a/src/Rules/Properties/AccessStaticPropertiesRule.php b/src/Rules/Properties/AccessStaticPropertiesRule.php index 50621db1de..a5c6f61e2c 100644 --- a/src/Rules/Properties/AccessStaticPropertiesRule.php +++ b/src/Rules/Properties/AccessStaticPropertiesRule.php @@ -9,7 +9,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\Rule; use PHPStan\Rules\RuleError; @@ -40,7 +40,7 @@ class AccessStaticPropertiesRule implements Rule public function __construct( private ReflectionProvider $reflectionProvider, private RuleLevelHelper $ruleLevelHelper, - private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ClassNameCheck $classCheck, ) { } @@ -132,10 +132,10 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, $class, ))->discoveringSymbolsTip()->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 { diff --git a/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php b/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php index 697477c2ee..8ec66cb948 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) { diff --git a/src/Rules/Properties/ExistingClassesInPropertiesRule.php b/src/Rules/Properties/ExistingClassesInPropertiesRule.php index 1f4780b33b..aa50b84475 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 { @@ -77,12 +72,13 @@ public function processNode(Node $node, Scope $scope): array ))->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() 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..b77fa6d94c 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 []; diff --git a/src/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRule.php b/src/Rules/Properties/MissingReadOnlyByPhpDocPropertyAssignRule.php index 9a52f0f99c..20bad45239 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 = []; @@ -48,7 +44,7 @@ public function processNode(Node $node, Scope $scope): array ))->line($propertyNode->getLine())->build(); } - foreach ($prematureAccess as [$propertyName, $line, $propertyNode]) { + foreach ($prematureAccess as [$propertyName, $line, $propertyNode, $file, $fileDescription]) { if (!$propertyNode->isReadOnlyByPhpDoc() || $propertyNode->isReadOnly()) { continue; } @@ -56,7 +52,7 @@ 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)->build(); } foreach ($additionalAssigns as [$propertyName, $line, $propertyNode]) { diff --git a/src/Rules/Properties/MissingReadOnlyPropertyAssignRule.php b/src/Rules/Properties/MissingReadOnlyPropertyAssignRule.php index caba4ba063..645978d7d8 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 = []; @@ -48,7 +44,7 @@ public function processNode(Node $node, Scope $scope): array ))->line($propertyNode->getLine())->build(); } - foreach ($prematureAccess as [$propertyName, $line, $propertyNode]) { + foreach ($prematureAccess as [$propertyName, $line, $propertyNode, $file, $fileDescription]) { if (!$propertyNode->isReadOnly()) { continue; } @@ -56,7 +52,7 @@ 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)->build(); } foreach ($additionalAssigns as [$propertyName, $line, $propertyNode]) { diff --git a/src/Rules/Properties/NullsafePropertyFetchRule.php b/src/Rules/Properties/NullsafePropertyFetchRule.php index 92fcf0e7fc..ecdf78650e 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 []; } diff --git a/src/Rules/Properties/OverridingPropertyRule.php b/src/Rules/Properties/OverridingPropertyRule.php index e8749b62f0..d4677ca63e 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 []; @@ -121,7 +116,7 @@ public function processNode(Node $node, Scope $scope): array $prototype->getNativeType()->describe(VerbosityLevel::typeOnly()), ))->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.', @@ -139,7 +134,7 @@ 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(); diff --git a/src/Rules/Properties/PropertiesInInterfaceRule.php b/src/Rules/Properties/PropertiesInInterfaceRule.php new file mode 100644 index 0000000000..16e06b4324 --- /dev/null +++ b/src/Rules/Properties/PropertiesInInterfaceRule.php @@ -0,0 +1,33 @@ + + */ +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()->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/ReadOnlyByPhpDocPropertyAssignRule.php b/src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRule.php index 4eeeb13f50..2dce93635f 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; @@ -76,7 +76,7 @@ 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) { + 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()))->build(); } diff --git a/src/Rules/Properties/ReadOnlyPropertyAssignRule.php b/src/Rules/Properties/ReadOnlyPropertyAssignRule.php index 2cc255dd4a..88dde3f612 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; @@ -76,7 +76,7 @@ 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) { + 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()))->build(); } 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..6db268642f 100644 --- a/src/Rules/Properties/ReadingWriteOnlyPropertiesRule.php +++ b/src/Rules/Properties/ReadingWriteOnlyPropertiesRule.php @@ -59,7 +59,7 @@ 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( diff --git a/src/Rules/Properties/TypesAssignedToPropertiesRule.php b/src/Rules/Properties/TypesAssignedToPropertiesRule.php index 6619ea81ab..9cf50b0584 100644 --- a/src/Rules/Properties/TypesAssignedToPropertiesRule.php +++ b/src/Rules/Properties/TypesAssignedToPropertiesRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\PropertyAssignNode; +use PHPStan\Reflection\PropertyReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; @@ -21,7 +22,6 @@ class TypesAssignedToPropertiesRule implements Rule public function __construct( private RuleLevelHelper $ruleLevelHelper, - private PropertyDescriptor $propertyDescriptor, private PropertyReflectionFinder $propertyReflectionFinder, ) { @@ -55,13 +55,17 @@ private function processSingleProperty( Node\Expr $assignedExpr, ): array { + if (!$propertyReflection->isWritable()) { + return []; + } + $propertyType = $propertyReflection->getWritableType(); $scope = $propertyReflection->getScope(); $assignedValueType = $scope->getType($assignedExpr); $accepts = $this->ruleLevelHelper->acceptsWithReason($propertyType, $assignedValueType, $scope->isDeclareStrictTypes()); if (!$accepts->result) { - $propertyDescription = $this->propertyDescriptor->describePropertyByName($propertyReflection, $propertyReflection->getName()); + $propertyDescription = $this->describePropertyByName($propertyReflection, $propertyReflection->getName()); $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($propertyType, $assignedValueType); return [ @@ -77,4 +81,13 @@ private function processSingleProperty( 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..bef3fd08ff 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 = []; @@ -48,7 +44,7 @@ public function processNode(Node $node, Scope $scope): array ))->line($propertyNode->getLine())->build(); } - foreach ($prematureAccess as [$propertyName, $line, $propertyNode]) { + foreach ($prematureAccess as [$propertyName, $line, $propertyNode, $file, $fileDescription]) { if ($propertyNode->isReadOnly() || $propertyNode->isReadOnlyByPhpDoc()) { continue; } @@ -56,7 +52,7 @@ 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)->build(); } return $errors; diff --git a/src/Rules/Properties/WritingToReadOnlyPropertiesRule.php b/src/Rules/Properties/WritingToReadOnlyPropertiesRule.php index f25cd413c7..e6828cb24e 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,7 +51,7 @@ 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( 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 771656f4cb..f5a1347d17 100644 --- a/src/Rules/RuleErrorBuilder.php +++ b/src/Rules/RuleErrorBuilder.php @@ -26,6 +26,9 @@ class RuleErrorBuilder /** @var mixed[] */ private array $properties; + /** @var list */ + private array $tips = []; + private function __construct(string $message) { $this->properties['message'] = $message; @@ -33,52 +36,79 @@ 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, + [], ], ]; } @@ -96,9 +126,10 @@ public function line(int $line): self return $this; } - public function file(string $file): self + public function file(string $file, ?string $fileDescription = null): self { $this->properties['file'] = $file; + $this->properties['fileDescription'] = $fileDescription ?? $file; $this->type |= self::TYPE_FILE; return $this; @@ -106,7 +137,15 @@ public function file(string $file): self public function tip(string $tip): self { - $this->properties['tip'] = $tip; + $this->tips = [$tip]; + $this->type |= self::TYPE_TIP; + + return $this; + } + + public function addTip(string $tip): self + { + $this->tips[] = $tip; $this->type |= self::TYPE_TIP; return $this; @@ -122,15 +161,11 @@ public function discoveringSymbolsTip(): self */ public function acceptsReasonsTip(array $reasons): self { - if (count($reasons) === 0) { - return $this; - } - - if (count($reasons) === 1) { - return $this->tip($reasons[0]); + foreach ($reasons as $reason) { + $this->addTip($reason); } - return $this->tip(implode("\n", array_map(static fn (string $reason) => sprintf('• %s', $reason), $reasons))); + return $this; } public function identifier(string $identifier): self @@ -172,6 +207,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 61ca2e851e..1a8a229c2e 100644 --- a/src/Rules/RuleLevelHelper.php +++ b/src/Rules/RuleLevelHelper.php @@ -10,6 +10,7 @@ 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; @@ -19,12 +20,13 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeTraverser; +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 { @@ -62,7 +64,13 @@ private function transformCommonType(Type $type): Type return TypeTraverser::map($type, function (Type $type, callable $traverse) { if ($type instanceof TemplateMixedType) { - return $type->toStrictMixedType(); + if (!$this->newRuleLevelHelper) { + return $type->toStrictMixedType(); + } + + if ($this->checkExplicitMixed) { + return $type->toStrictMixedType(); + } } if ( $type instanceof MixedType @@ -104,6 +112,7 @@ private function transformAcceptedType(Type $acceptingType, Type $acceptedType): $acceptedType->isVariadic(), $acceptedType->getTemplateTypeMap(), $acceptedType->getResolvedTemplateTypeMap(), + $acceptedType->getCallSiteVarianceMap(), ); } @@ -119,7 +128,7 @@ private function transformAcceptedType(Type $acceptingType, Type $acceptedType): if ($this->checkBenevolentUnionTypes) { if ($acceptedType instanceof BenevolentUnionType) { $checkForUnion = true; - return $traverse(new UnionType($acceptedType->getTypes())); + return $traverse(TypeUtils::toStrictUnion($acceptedType)); } } @@ -149,7 +158,7 @@ public function acceptsWithReason(Type $acceptingType, Type $acceptedType, bool $traverse = static function (Type $type, callable $traverse) use (&$checkForUnion): Type { if ($type instanceof BenevolentUnionType) { $checkForUnion = true; - return new UnionType($type->getTypes()); + return TypeUtils::toStrictUnion($type); } return $traverse($type); @@ -295,52 +304,89 @@ public function findTypeToCheck( return new FoundTypeResult(new ErrorType(), [], [], null); } $type = $scope->getType($var); + + 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 = $type->getObjectClassNames(); $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->getLine())->discoveringSymbolsTip()->build(); } - - $errors[] = RuleErrorBuilder::message(sprintf($unknownClassErrorPattern, $referencedClass))->line($var->getLine())->discoveringSymbolsTip()->build(); } if (count($errors) > 0 || $hasClassExistsClass) { @@ -351,33 +397,81 @@ public function findTypeToCheck( return new FoundTypeResult(new ErrorType(), [], [], null); } - if ( - ( - !$this->checkUnionTypes - && $type instanceof UnionType - && !$type instanceof BenevolentUnionType - ) || ( - !$this->checkBenevolentUnionTypes - && $type instanceof BenevolentUnionType - ) - ) { - $newTypes = []; + if ($this->newRuleLevelHelper) { + if ($type instanceof UnionType) { + $shouldFilterUnion = ( + !$this->checkUnionTypes + && !$type instanceof BenevolentUnionType + ) || ( + !$this->checkBenevolentUnionTypes + && $type instanceof BenevolentUnionType + ); - foreach ($type->getTypes() as $innerType) { - if (!$unionTypeCriteriaCallback($innerType)) { - continue; + $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(); } - $newTypes[] = $innerType; + 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; + } - if (count($newTypes) > 0) { - return new FoundTypeResult(TypeCombinator::union(...$newTypes), $directClassNames, [], null); + $newTypes[] = $innerType; + } + + if (count($newTypes) > 0) { + return new FoundTypeResult(TypeCombinator::union(...$newTypes), $directClassNames, [], null); + } } } $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/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 63eb0662e5..98b4f538a3 100644 --- a/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php @@ -24,20 +24,21 @@ 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->isNull()->yes()) { return []; diff --git a/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php index d5cf38c3ea..b697039019 100644 --- a/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php @@ -26,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 []; @@ -46,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(); 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 8a0be0f2b7..31d2d7a351 100644 --- a/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php @@ -5,12 +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\TypeCombinator; +use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function count; @@ -29,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 []; } 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 c851cbf2ae..64c6acc3cf 100644 --- a/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php @@ -5,13 +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\TypeCombinator; +use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function count; @@ -23,7 +22,7 @@ class TooWideMethodReturnTypehintRule implements Rule { - public function __construct(private bool $checkProtectedAndPublicMethods) + public function __construct(private bool $checkProtectedAndPublicMethods, private bool $alwaysCheckFinal) { } @@ -34,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 []; } diff --git a/src/Rules/TooWideTypehints/TooWideParameterOutTypeCheck.php b/src/Rules/TooWideTypehints/TooWideParameterOutTypeCheck.php new file mode 100644 index 0000000000..4d2377ead1 --- /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 array + */ + 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', + )); + 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..229693e088 --- /dev/null +++ b/src/Rules/Traits/ConflictingTraitConstantsRule.php @@ -0,0 +1,212 @@ + + */ +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()->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()->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()->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()->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()->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()->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()->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()->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()->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()->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()->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()->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..e5468e59f1 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); diff --git a/src/Rules/Types/InvalidTypesInUnionRule.php b/src/Rules/Types/InvalidTypesInUnionRule.php new file mode 100644 index 0000000000..750982b8e8 --- /dev/null +++ b/src/Rules/Types/InvalidTypesInUnionRule.php @@ -0,0 +1,115 @@ + + */ +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 && in_array($type->toString(), self::ONLY_STANDALONE_TYPES, true)) { + return [ + RuleErrorBuilder::message(sprintf('Type %s cannot be part of a union type declaration.', $type->toString())) + ->line($complexType->getLine()) + ->nonIgnorable() + ->build(), + ]; + } + } + + return []; + } + + if ($complexType->type instanceof Node\Identifier && in_array($complexType->type->toString(), self::ONLY_STANDALONE_TYPES, true)) { + return [ + RuleErrorBuilder::message(sprintf('Type %s cannot be part of a nullable type declaration.', $complexType->type->toString())) + ->line($complexType->getLine()) + ->nonIgnorable() + ->build(), + ]; + } + + return []; + } + +} diff --git a/src/Rules/UnusedFunctionParametersCheck.php b/src/Rules/UnusedFunctionParametersCheck.php index 66ba1ad27a..d7d256f3f1 100644 --- a/src/Rules/UnusedFunctionParametersCheck.php +++ b/src/Rules/UnusedFunctionParametersCheck.php @@ -63,7 +63,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/EmptyRule.php b/src/Rules/Variables/EmptyRule.php index 9a48592cb9..6625da896f 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; /** @@ -28,11 +26,11 @@ 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()); + $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..ff149ba5f8 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; /** @@ -29,7 +28,7 @@ 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); + $isNull = $type->isNull(); if ($isNull->maybe()) { return null; } diff --git a/src/Rules/Variables/NullCoalesceRule.php b/src/Rules/Variables/NullCoalesceRule.php index 8666ab4f7e..3e52e3bfd6 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; } diff --git a/src/Rules/Variables/ParameterOutAssignedTypeRule.php b/src/Rules/Variables/ParameterOutAssignedTypeRule.php new file mode 100644 index 0000000000..e08d953d87 --- /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), + )); + + 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..bb8865ce33 --- /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 array + */ + 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/Whitespace/FileWhitespaceRule.php b/src/Rules/Whitespace/FileWhitespaceRule.php index 9a48d9293a..5bda8b5d68 100644 --- a/src/Rules/Whitespace/FileWhitespaceRule.php +++ b/src/Rules/Whitespace/FileWhitespaceRule.php @@ -43,7 +43,7 @@ public function processNode(Node $node, Scope $scope): array private array $lastNodes = []; /** - * @return int|Node|null + * @return int|null */ public function enterNode(Node $node) { diff --git a/src/Testing/ErrorFormatterTestCase.php b/src/Testing/ErrorFormatterTestCase.php index 924765b90b..f4b90e6778 100644 --- a/src/Testing/ErrorFormatterTestCase.php +++ b/src/Testing/ErrorFormatterTestCase.php @@ -101,6 +101,8 @@ protected function getAnalysisResult(int $numFileErrors, int $numGenericErrors): 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 4f2dd8449a..14efcc7480 100644 --- a/src/Testing/PHPStanTestCase.php +++ b/src/Testing/PHPStanTestCase.php @@ -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; @@ -37,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; @@ -65,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__ . '/../..'; @@ -106,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'); @@ -123,7 +126,7 @@ public function createBroker(): Broker } /** @api */ - public function createReflectionProvider(): ReflectionProvider + public static function createReflectionProvider(): ReflectionProvider { return self::getContainer()->getByType(ReflectionProvider::class); } @@ -146,7 +149,7 @@ public static function getReflectors(): array ]; } - public function getClassReflectionExtensionRegistryProvider(): ClassReflectionExtensionRegistryProvider + public static function getClassReflectionExtensionRegistryProvider(): ClassReflectionExtensionRegistryProvider { return self::getContainer()->getByType(ClassReflectionExtensionRegistryProvider::class); } @@ -154,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(); @@ -169,12 +172,20 @@ public function createScopeFactory(ReflectionProvider $reflectionProvider, TypeS new DirectInternalScopeFactory( MutatingScope::class, $reflectionProvider, - new InitializerExprTypeResolver($constantResolver, $reflectionProviderProvider, new PhpVersion(PHP_VERSION_ID), $container->getByType(OperatorTypeSpecifyingExtensionRegistryProvider::class), new OversizedArrayBuilder(), $container->getParameter('usePathConstantsAsConstantString')), + 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(), - $this->getParser(), + self::getParser(), $container->getByType(NodeScopeResolver::class), $container->getByType(PhpVersion::class), $container->getParameter('featureToggles')['explicitMixedInUnknownGenericNew'], @@ -187,7 +198,7 @@ public function createScopeFactory(ReflectionProvider $reflectionProvider, TypeS /** * @param array $globalTypeAliases */ - public function createTypeAliasResolver(array $globalTypeAliases, ReflectionProvider $reflectionProvider): TypeAliasResolver + public static function createTypeAliasResolver(array $globalTypeAliases, ReflectionProvider $reflectionProvider): TypeAliasResolver { $container = self::getContainer(); @@ -204,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 ce7b3ce97d..c850320460 100644 --- a/src/Testing/RuleTestCase.php +++ b/src/Testing/RuleTestCase.php @@ -20,6 +20,7 @@ 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; @@ -82,22 +83,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), @@ -121,7 +127,7 @@ private function getAnalyser(): Analyser /** * @param string[] $files - * @param list $expectedErrors + * @param list $expectedErrors */ public function analyse(array $files, array $expectedErrors): void { @@ -179,7 +185,7 @@ public function gatherAnalyserErrors(array $files): array ]); $nodeType = CollectedDataNode::class; - $node = new CollectedDataNode($analyserResult->getCollectedData()); + $node = new CollectedDataNode($analyserResult->getCollectedData(), false); $scopeFactory = $this->createScopeFactory($this->createReflectionProvider(), $this->getTypeSpecifier()); $scope = $scopeFactory->create(ScopeContext::create('irrelevant')); foreach ($ruleRegistry->getRules($nodeType) as $rule) { 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 288db71f2f..14fdb7b02e 100644 --- a/src/Testing/TypeInferenceTestCase.php +++ b/src/Testing/TypeInferenceTestCase.php @@ -14,16 +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 @@ -33,43 +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), + self::createScopeFactory($reflectionProvider, $typeSpecifier), + self::getContainer()->getParameter('polluteScopeWithLoopInitialAssignments'), + self::getContainer()->getParameter('polluteScopeWithAlwaysIterableForeach'), + static::getEarlyTerminatingMethodCalls(), + static::getEarlyTerminatingFunctionCalls(), + self::getContainer()->getParameter('universalObjectCratesClasses'), true, - true, - $this->getEarlyTerminatingMethodCalls(), - $this->getEarlyTerminatingFunctionCalls(), - true, - $this->shouldTreatPhpDocTypesAsCertain(), + 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, ); @@ -86,11 +96,18 @@ public function assertFileAsserts( ): void { if ($assertType === 'type') { - $expectedType = $args[0]; - $this->assertInstanceOf(ConstantScalarType::class, $expectedType); - $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, @@ -111,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; } @@ -125,49 +142,86 @@ 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->getLine(), + )); + } 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->getLine()]; } elseif ($functionName === 'PHPStan\\Testing\\assertNativeType') { $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, $actualType, $node->getLine()]; + $assert = ['type', $file, $expectedType->getValue(), $actualType->describe(VerbosityLevel::precise()), $node->getLine()]; } 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 $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, $node->getLine()]; } 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->getLine(), + )); } if (count($node->getArgs()) !== 2) { - $this->fail(sprintf( + self::fail(sprintf( 'ERROR: Wrong %s() call on line %d.', $functionName, $node->getLine(), @@ -178,26 +232,26 @@ public function gatherAssertTypes(string $file): array }); if (count($asserts) === 0) { - $this->fail(sprintf('File %s does not contain any asserts', $file)); + 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 5c4c3782fa..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 { diff --git a/src/Type/AcceptsResult.php b/src/Type/AcceptsResult.php index 9ee2c70053..fd30d8badd 100644 --- a/src/Type/AcceptsResult.php +++ b/src/Type/AcceptsResult.php @@ -6,6 +6,8 @@ use PHPStan\TrinaryLogic; use function array_map; use function array_merge; +use function array_unique; +use function array_values; /** @api */ class AcceptsResult @@ -60,7 +62,7 @@ public function and(self $other): self { return new self( $this->result->and($other->result), - array_merge($this->reasons, $other->reasons), + array_values(array_unique(array_merge($this->reasons, $other->reasons))), ); } @@ -68,7 +70,7 @@ public function or(self $other): self { return new self( $this->result->or($other->result), - array_merge($this->reasons, $other->reasons), + array_values(array_unique(array_merge($this->reasons, $other->reasons))), ); } diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index 77fb5673c5..43ebf895c2 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -2,8 +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\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -22,7 +26,6 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; -use function sprintf; /** @api */ class AccessoryArrayListType implements CompoundType, AccessoryType @@ -52,6 +55,11 @@ public function getObjectClassNames(): array return []; } + public function getObjectClassReflections(): array + { + return []; + } + public function getArrays(): array { return []; @@ -80,17 +88,8 @@ public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult $isArray = $type->isArray(); $isList = $type->isList(); - $reasons = []; - if ($isArray->yes() && !$isList->yes()) { - $verbosity = VerbosityLevel::getRecommendedLevelByType($this, $type); - $reasons[] = sprintf( - '%s %s a list.', - $type->describe($verbosity), - $isList->no() ? 'is not' : 'might not be', - ); - } - return new AcceptsResult($isArray->and($isList), $reasons); + return new AcceptsResult($isArray->and($isList), []); } public function isSuperTypeOf(Type $type): TrinaryLogic @@ -162,6 +161,15 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni 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()) { @@ -290,6 +298,26 @@ 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(); @@ -345,6 +373,16 @@ 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(); @@ -355,6 +393,11 @@ 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(); @@ -396,6 +439,11 @@ 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(); @@ -425,4 +473,14 @@ 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 5b56a1ea79..3fd1b8a356 100644 --- a/src/Type/Accessory/AccessoryLiteralStringType.php +++ b/src/Type/Accessory/AccessoryLiteralStringType.php @@ -2,6 +2,9 @@ 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; @@ -15,6 +18,7 @@ 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; @@ -53,6 +57,11 @@ public function getObjectClassNames(): array return []; } + public function getObjectClassReflections(): array + { + return []; + } + public function getConstantStrings(): array { return []; @@ -125,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 @@ -142,6 +151,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 new ErrorType(); @@ -179,7 +193,7 @@ public function toArray(): Type [$this], [1], [], - true, + TrinaryLogic::createYes(), ); } @@ -193,6 +207,26 @@ 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(); @@ -248,6 +282,16 @@ 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(); @@ -263,11 +307,21 @@ 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(); @@ -286,4 +340,14 @@ public function exponentiate(Type $exponent): Type ]); } + 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 e99af7a77a..771dec0e22 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -2,9 +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; @@ -14,6 +18,7 @@ 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; @@ -53,6 +58,11 @@ public function getObjectClassNames(): array return []; } + public function getObjectClassReflections(): array + { + return []; + } + public function getConstantStrings(): array { return []; @@ -126,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 @@ -147,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 { return new ErrorType(); @@ -179,7 +194,7 @@ public function toArray(): Type [$this], [1], [], - true, + TrinaryLogic::createYes(), ); } @@ -193,6 +208,26 @@ 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(); @@ -248,6 +283,16 @@ 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(); @@ -258,11 +303,21 @@ 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(); @@ -290,4 +345,14 @@ public function exponentiate(Type $exponent): Type ]); } + 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 8b6c0c01d1..cf4402f059 100644 --- a/src/Type/Accessory/AccessoryNonFalsyStringType.php +++ b/src/Type/Accessory/AccessoryNonFalsyStringType.php @@ -2,18 +2,22 @@ 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; @@ -54,6 +58,11 @@ public function getObjectClassNames(): array return []; } + public function getObjectClassReflections(): array + { + return []; + } + public function getConstantStrings(): array { return []; @@ -127,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 @@ -144,6 +153,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 new ErrorType(); @@ -156,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 @@ -179,7 +190,7 @@ public function toArray(): Type [$this], [1], [], - true, + TrinaryLogic::createYes(), ); } @@ -193,6 +204,26 @@ 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(); @@ -248,6 +279,16 @@ 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(); @@ -258,11 +299,21 @@ 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(); @@ -281,4 +332,14 @@ public function exponentiate(Type $exponent): Type ]); } + 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 59bfd7e71a..a15078542b 100644 --- a/src/Type/Accessory/AccessoryNumericStringType.php +++ b/src/Type/Accessory/AccessoryNumericStringType.php @@ -2,9 +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; @@ -53,6 +57,11 @@ public function getObjectClassNames(): array return []; } + public function getObjectClassReflections(): array + { + return []; + } + public function getConstantStrings(): array { return []; @@ -130,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 @@ -147,6 +156,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 new ErrorType(); @@ -182,7 +196,7 @@ public function toArray(): Type [$this], [1], [], - true, + TrinaryLogic::createYes(), ); } @@ -196,6 +210,26 @@ 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(); @@ -251,6 +285,16 @@ 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(); @@ -261,11 +305,21 @@ 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(); @@ -293,4 +347,14 @@ public function exponentiate(Type $exponent): Type ]); } + 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 43176d50d7..070f853329 100644 --- a/src/Type/Accessory/HasMethodType.php +++ b/src/Type/Accessory/HasMethodType.php @@ -2,6 +2,8 @@ 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\ExtendedMethodReflection; @@ -49,6 +51,11 @@ public function getObjectClassNames(): array return []; } + public function getObjectClassReflections(): array + { + return []; + } + private function getCanonicalMethodName(): string { return strtolower($this->methodName); @@ -173,14 +180,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($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 243dba8a51..2681b9ebb5 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -2,15 +2,19 @@ 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; @@ -64,6 +68,11 @@ public function getObjectClassNames(): array return []; } + public function getObjectClassReflections(): array + { + return []; + } + public function getConstantStrings(): array { return []; @@ -131,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(); } @@ -148,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()) { @@ -194,6 +208,26 @@ 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(); @@ -249,6 +283,16 @@ 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(); @@ -259,6 +303,11 @@ 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(); @@ -309,14 +358,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($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 e826ef2cd4..d230bbff48 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -2,9 +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; @@ -12,6 +16,7 @@ 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; @@ -63,6 +68,11 @@ public function getObjectClassNames(): array return []; } + public function getObjectClassReflections(): array + { + return []; + } + public function getConstantStrings(): array { return []; @@ -138,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(); } @@ -147,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; } @@ -171,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()) { @@ -249,6 +264,26 @@ 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(); @@ -304,6 +339,16 @@ 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(); @@ -314,6 +359,11 @@ 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(); @@ -359,14 +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 52cac40ed8..53170faac3 100644 --- a/src/Type/Accessory/HasPropertyType.php +++ b/src/Type/Accessory/HasPropertyType.php @@ -2,6 +2,8 @@ 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; @@ -46,6 +48,11 @@ public function getObjectClassNames(): array return []; } + public function getObjectClassReflections(): array + { + return []; + } + public function getConstantStrings(): array { return []; @@ -135,14 +142,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($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 f8b35eba8a..cdd7d0b803 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -2,8 +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\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -21,7 +25,6 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; -use function sprintf; class NonEmptyArrayType implements CompoundType, AccessoryType { @@ -49,6 +52,11 @@ public function getObjectClassNames(): array return []; } + public function getObjectClassReflections(): array + { + return []; + } + public function getArrays(): array { return []; @@ -77,17 +85,8 @@ public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult $isArray = $type->isArray(); $isIterableAtLeastOnce = $type->isIterableAtLeastOnce(); - $reasons = []; - if ($isArray->yes() && !$isIterableAtLeastOnce->yes()) { - $verbosity = VerbosityLevel::getRecommendedLevelByType($this, $type); - $reasons[] = sprintf( - '%s %s empty.', - $type->describe($verbosity), - $isIterableAtLeastOnce->no() ? 'is' : 'might be', - ); - } - return new AcceptsResult($isArray->and($isIterableAtLeastOnce), $reasons); + return new AcceptsResult($isArray->and($isIterableAtLeastOnce), []); } public function isSuperTypeOf(Type $type): TrinaryLogic @@ -155,6 +154,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 new ErrorType(); @@ -275,6 +279,26 @@ 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(); @@ -330,6 +354,16 @@ 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(); @@ -340,6 +374,11 @@ 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(); @@ -362,7 +401,7 @@ public function toString(): Type public function toArray(): Type { - return new MixedType(); + return $this; } public function toArrayKey(): Type @@ -375,14 +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 56ab95616e..dbab26f8d9 100644 --- a/src/Type/Accessory/OversizedArrayType.php +++ b/src/Type/Accessory/OversizedArrayType.php @@ -2,8 +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\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -47,6 +51,11 @@ public function getObjectClassNames(): array return []; } + public function getObjectClassReflections(): array + { + return []; + } + public function getArrays(): array { return []; @@ -141,6 +150,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 new ErrorType(); @@ -261,6 +275,26 @@ 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(); @@ -316,6 +350,16 @@ 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(); @@ -326,6 +370,11 @@ 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(); @@ -361,14 +410,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(''); // no PHPDoc representation + } + } diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index c7410331ad..2368793e1b 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -2,6 +2,10 @@ 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; @@ -18,7 +22,6 @@ 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; @@ -27,6 +30,7 @@ use PHPStan\Type\Traits\UndecidedBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; use function array_merge; +use function count; use function sprintf; /** @api */ @@ -76,6 +80,11 @@ public function getObjectClassNames(): array return []; } + public function getObjectClassReflections(): array + { + return []; + } + public function getArrays(): array { return [$this]; @@ -123,7 +132,7 @@ 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) { @@ -280,6 +289,26 @@ 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(); @@ -335,6 +364,16 @@ 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(); @@ -345,6 +384,11 @@ 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(); @@ -353,7 +397,10 @@ public function isOffsetAccessible(): TrinaryLogic public function hasOffsetValueType(Type $offsetType): TrinaryLogic { $offsetType = $offsetType->toArrayKey(); - if ($this->getKeyType()->isSuperTypeOf($offsetType)->no()) { + + if ($this->getKeyType()->isSuperTypeOf($offsetType)->no() + && ($offsetType->isString()->no() || !$offsetType->isConstantScalarValue()->no()) + ) { return TrinaryLogic::createNo(); } @@ -363,7 +410,9 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic public function getOffsetValueType(Type $offsetType): Type { $offsetType = $offsetType->toArrayKey(); - if ($this->getKeyType()->isSuperTypeOf($offsetType)->no()) { + if ($this->getKeyType()->isSuperTypeOf($offsetType)->no() + && ($offsetType->isString()->no() || !$offsetType->isConstantScalarValue()->no()) + ) { return new ErrorType(); } @@ -378,7 +427,31 @@ 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 = $offsetType->toArrayKey(); } @@ -409,6 +482,14 @@ 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 = $offsetType->toArrayKey(); @@ -431,7 +512,7 @@ public function unsetOffset(Type $offsetType): Type public function fillKeysArray(Type $valueType): Type { $itemType = $this->getItemType(); - if ((new IntegerType())->isSuperTypeOf($itemType)->no()) { + if ($itemType->isInteger()->no()) { $stringKeyType = $itemType->toString(); if ($stringKeyType instanceof ErrorType) { return $stringKeyType; @@ -556,7 +637,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); @@ -567,31 +648,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), + ); + } + + public function traverse(callable $cb): Type + { + $keyType = $cb($this->keyType); + $itemType = $cb($this->itemType); - if (!$positionVariance->contravariant()) { - $keyType = $this->getKeyType(); - if ($keyType instanceof TemplateType) { - $keyVariance = $keyType->getVariance(); + 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) { @@ -630,6 +741,11 @@ 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 80fe01896b..dc1245ad45 100644 --- a/src/Type/BenevolentUnionType.php +++ b/src/Type/BenevolentUnionType.php @@ -43,11 +43,18 @@ protected function unionTypes(callable $getType): Type return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$resultTypes)); } - protected function pickFromTypes(callable $getValues): array + 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; } @@ -137,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 89fc67754c..c0ac5b5920 100644 --- a/src/Type/BooleanType.php +++ b/src/Type/BooleanType.php @@ -2,6 +2,9 @@ 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; @@ -84,7 +87,7 @@ public function toArray(): Type [$this], [1], [], - true, + TrinaryLogic::createYes(), ); } @@ -118,6 +121,11 @@ 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) { @@ -127,11 +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 2c0b3b25d9..e4c88e1c1e 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -3,16 +3,27 @@ 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\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; @@ -23,8 +34,7 @@ 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 @@ -46,19 +56,37 @@ class CallableType implements CompoundType, ParametersAcceptor private bool $isCommonCallable; + private TemplateTypeMap $templateTypeMap; + + private TemplateTypeMap $resolvedTemplateTypeMap; + /** * @api * @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 = [], ) { $this->parameters = $parameters ?? []; $this->returnType = $returnType ?? new MixedType(); $this->isCommonCallable = $parameters === null && $returnType === null; + $this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty(); + $this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap ?? TemplateTypeMap::createEmpty(); + } + + /** + * @return array + */ + public function getTemplateTags(): array + { + return $this->templateTags; } /** @@ -79,6 +107,11 @@ public function getObjectClassNames(): array return []; } + public function getObjectClassReflections(): array + { + return []; + } + public function getConstantStrings(): array { return []; @@ -95,7 +128,7 @@ public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult return $type->isAcceptedWithReasonBy($this, $strictTypes); } - return new AcceptsResult($this->isSuperTypeOfInternal($type, true), []); + return $this->isSuperTypeOfInternal($type, true); } public function isSuperTypeOf(Type $type): TrinaryLogic @@ -104,12 +137,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 $treatMixedAsAny): TrinaryLogic + private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): AcceptsResult { - $isCallable = $type->isCallable(); + $isCallable = new AcceptsResult($type->isCallable(), []); if ($isCallable->no() || $this->isCommonCallable) { return $isCallable; } @@ -165,19 +198,26 @@ 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, + ); + + return $printer->print($selfWithoutParameterNames->toPhpDocNode()); + }, ); } @@ -226,12 +266,17 @@ public function toArrayKey(): Type 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(); } /** @@ -334,6 +379,58 @@ public function traverse(callable $cb): Type $parameters, $cb($this->getReturnType()), $this->isVariadic(), + $this->templateTypeMap, + $this->resolvedTemplateTypeMap, + $this->templateTags, + ); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + 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, ); } @@ -347,6 +444,26 @@ 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(); @@ -402,6 +519,16 @@ 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(); @@ -412,6 +539,11 @@ public function isScalar(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function getEnumCases(): array { return []; @@ -427,6 +559,45 @@ public function exponentiate(Type $exponent): Type return new ErrorType(); } + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + if ($this->isCommonCallable) { + return new IdentifierTypeNode('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('callable'), + $parameters, + $this->returnType->toPhpDocNode(), + $templateTags, + ); + } + /** * @param mixed[] $properties */ diff --git a/src/Type/CallableTypeHelper.php b/src/Type/CallableTypeHelper.php index f45c72ed7f..1023796444 100644 --- a/src/Type/CallableTypeHelper.php +++ b/src/Type/CallableTypeHelper.php @@ -4,6 +4,10 @@ use PHPStan\Reflection\ParametersAcceptor; use PHPStan\TrinaryLogic; +use function array_key_exists; +use function array_merge; +use function count; +use function sprintf; class CallableTypeHelper { @@ -12,53 +16,94 @@ public static function isParametersAcceptorSuperTypeOf( ParametersAcceptor $ours, ParametersAcceptor $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); - } else { - $isReturnTypeSuperType = $ours->getReturnType()->isSuperTypeOf($theirReturnType); - } - if ($result === null) { - $result = $isReturnTypeSuperType; + $isReturnTypeSuperType = $ours->getReturnType()->acceptsWithReason($theirReturnType, true); } else { - $result = $result->and($isReturnTypeSuperType); + $isReturnTypeSuperType = new AcceptsResult($ours->getReturnType()->isSuperTypeOf($theirReturnType), []); } - return $result; + return $result->and($isReturnTypeSuperType); } } diff --git a/src/Type/ClassStringType.php b/src/Type/ClassStringType.php index a198e85c25..802d17e162 100644 --- a/src/Type/ClassStringType.php +++ b/src/Type/ClassStringType.php @@ -2,6 +2,8 @@ namespace PHPStan\Type; +use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\TrinaryLogic; /** @api */ @@ -72,6 +74,21 @@ 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 6b00755315..91e27bd209 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -4,6 +4,14 @@ use Closure; 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\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ConstantReflection; @@ -11,7 +19,9 @@ 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,6 +32,7 @@ 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; @@ -31,8 +42,7 @@ 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 @@ -52,9 +62,12 @@ class ClosureType implements TypeWithClassName, ParametersAcceptor private TemplateTypeMap $resolvedTemplateTypeMap; + private TemplateTypeVarianceMap $callSiteVarianceMap; + /** * @api * @param array $parameters + * @param array $templateTags */ public function __construct( private array $parameters, @@ -62,11 +75,22 @@ public function __construct( private bool $variadic, ?TemplateTypeMap $templateTypeMap = null, ?TemplateTypeMap $resolvedTemplateTypeMap = null, + ?TemplateTypeVarianceMap $callSiteVarianceMap = null, + private array $templateTags = [], ) { $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 getClassName(): string @@ -99,7 +123,12 @@ public function getReferencedClasses(): array public function getObjectClassNames(): array { - return [$this->objectType->getClassName()]; + return $this->objectType->getObjectClassNames(); + } + + public function getObjectClassReflections(): array + { + return $this->objectType->getObjectClassReflections(); } public function accepts(Type $type, bool $strictTypes): TrinaryLogic @@ -117,7 +146,7 @@ public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult return $this->objectType->acceptsWithReason($type, $strictTypes); } - return new AcceptsResult($this->isSuperTypeOfInternal($type, true), []); + return $this->isSuperTypeOfInternal($type, true); } public function isSuperTypeOf(Type $type): TrinaryLogic @@ -126,10 +155,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( @@ -140,10 +169,10 @@ private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): Trina } if ($type->getObjectClassNames() === [Closure::class]) { - return TrinaryLogic::createMaybe(); + return AcceptsResult::createMaybe(); } - return $this->objectType->isSuperTypeOf($type); + return new AcceptsResult($this->objectType->isSuperTypeOf($type), []); } public function equals(Type $type): bool @@ -159,19 +188,27 @@ 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 { + $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, + ); + + return $printer->print($selfWithoutParameterNames->toPhpDocNode()); + }, ); } @@ -180,6 +217,11 @@ 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); @@ -317,7 +359,7 @@ public function toArray(): Type [$this], [1], [], - true, + TrinaryLogic::createYes(), ); } @@ -336,6 +378,11 @@ public function getResolvedTemplateTypeMap(): TemplateTypeMap return $this->resolvedTemplateTypeMap; } + public function getCallSiteVarianceMap(): TemplateTypeVarianceMap + { + return $this->callSiteVarianceMap; + } + /** * @return array */ @@ -415,6 +462,49 @@ public function traverse(callable $cb): Type $this->isVariadic(), $this->templateTypeMap, $this->resolvedTemplateTypeMap, + $this->callSiteVarianceMap, + $this->templateTags, + ); + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + 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, ); } @@ -423,6 +513,26 @@ 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(); @@ -478,6 +588,16 @@ 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(); @@ -488,11 +608,51 @@ 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 + { + $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 */ @@ -504,6 +664,7 @@ public static function __set_state(array $properties): Type $properties['variadic'], $properties['templateTypeMap'], $properties['resolvedTemplateTypeMap'], + $properties['callSiteVarianceMap'], ); } 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 bb9d2d36d5..1d520429bc 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -2,10 +2,23 @@ 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\ClassMemberAccessAnswerer; use PHPStan\Reflection\InaccessibleMethod; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\ParametersAcceptor; -use PHPStan\Reflection\ReflectionProviderStaticAccessor; +use PHPStan\Reflection\PhpVersionStaticAccessor; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; @@ -20,20 +33,15 @@ 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; use PHPStan\Type\IntegerRangeType; -use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; -use PHPStan\Type\ObjectType; -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; @@ -49,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 @@ -65,6 +75,8 @@ class ConstantArrayType extends ArrayType implements ConstantType private const DESCRIBE_LIMIT = 8; + private TrinaryLogic $isList; + /** @var self[]|null */ private ?array $allArrays = null; @@ -83,7 +95,7 @@ public function __construct( private array $valueTypes, int|array $nextAutoIndexes = [0], private array $optionalKeys = [], - private bool $isList = false, + bool|TrinaryLogic $isList = false, ) { assert(count($keyTypes) === count($valueTypes)); @@ -97,13 +109,18 @@ public function __construct( $keyTypesCount = count($this->keyTypes); if ($keyTypesCount === 0) { $keyType = new NeverType(true); - $this->isList = 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), @@ -115,6 +132,11 @@ public function getConstantArrays(): array return [$this]; } + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + /** @deprecated Use isIterableAtLeastOnce()->no() instead */ public function isEmpty(): bool { @@ -176,7 +198,7 @@ public function getAllArrays(): array $keys = array_merge($requiredKeys, $combination); sort($keys); - if ($this->isList && array_keys($keys) !== array_values($keys)) { + if ($this->isList->yes() && array_keys($keys) !== $keys) { continue; } @@ -360,7 +382,7 @@ 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(); @@ -382,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) { @@ -395,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-next-line + return new ConstantBooleanType($type->getConstantScalarValues()[0] == []); // phpcs:ignore + } + + return new BooleanType(); + } + public function equals(Type $type): bool { if (!$type instanceof self) { @@ -468,38 +502,52 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) 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 ($classOrObject->isObject()->yes()) { - $type = $classOrObject; - } else { + $type = $classOrObject->getObjectTypeOrClassStringObjectType(); + if (!$type->isObject()->yes()) { return ConstantArrayTypeAndMethod::createUnknown(); } @@ -518,56 +566,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 ($classOrObject->isObject()->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; @@ -576,16 +613,13 @@ public function findTypeAndMethodNames(): array public function hasOffsetValueType(Type $offsetType): TrinaryLogic { $offsetType = $offsetType->toArrayKey(); - if ($offsetType instanceof UnionType) { - return TrinaryLogic::lazyExtremeIdentity($offsetType->getTypes(), fn (Type $innerType) => $this->hasOffsetValueType($innerType)); - } $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(); } @@ -649,6 +683,21 @@ 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 = $offsetType->toArrayKey(); @@ -677,13 +726,13 @@ public function unsetOffset(Type $offsetType): Type $k++; } - return new self($newKeyTypes, $newValueTypes, $this->nextAutoIndexes, $newOptionalKeys, false); + 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; @@ -706,7 +755,7 @@ public function unsetOffset(Type $offsetType): Type } } - return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $optionalKeys, false); + return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $optionalKeys, TrinaryLogic::createNo()); } $optionalKeys = $this->optionalKeys; @@ -716,7 +765,7 @@ public function unsetOffset(Type $offsetType): Type continue; } $optionalKeys[] = $i; - $isList = false; + $isList = TrinaryLogic::createNo(); } $optionalKeys = array_values(array_unique($optionalKeys)); @@ -728,7 +777,7 @@ public function fillKeysArray(Type $valueType): Type $builder = ConstantArrayTypeBuilder::createEmpty(); foreach ($this->valueTypes as $i => $keyType) { - if ((new IntegerType())->isSuperTypeOf($keyType)->no()) { + if ($keyType->isInteger()->no()) { $stringKeyType = $keyType->toString(); if ($stringKeyType instanceof ErrorType) { return $stringKeyType; @@ -826,12 +875,12 @@ public function shuffleArray(): Type return $valuesArray; } - $generalizedArray = new ArrayType($valuesArray->getKeyType(), $valuesArray->getItemType()); + $generalizedArray = new ArrayType($valuesArray->getIterableKeyType(), $valuesArray->getItemType()); if ($isIterableAtLeastOnce->yes()) { $generalizedArray = TypeCombinator::intersect($generalizedArray, new NonEmptyArrayType()); } - if ($valuesArray->isList) { + if ($valuesArray->isList->yes()) { $generalizedArray = AccessoryArrayListType::intersectWith($generalizedArray); } @@ -923,7 +972,7 @@ public function isConstantArray(): TrinaryLogic public function isList(): TrinaryLogic { - return TrinaryLogic::createFromBoolean($this->isList); + return $this->isList; } /** @deprecated Use popArray() instead */ @@ -1112,7 +1161,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, false); + $reversed = new self($keyTypes, array_reverse($this->valueTypes), $this->nextAutoIndexes, $optionalKeys, TrinaryLogic::createNo()); return $preserveKeys ? $reversed : $reversed->reindex(); } @@ -1151,7 +1200,7 @@ private function reindex(): self $autoIndex++; } - return new self($keyTypes, $this->valueTypes, [$autoIndex], $this->optionalKeys, true); + return new self($keyTypes, $this->valueTypes, [$autoIndex], $this->optionalKeys, TrinaryLogic::createYes()); } public function toBoolean(): BooleanType @@ -1180,7 +1229,7 @@ public function generalize(GeneralizePrecision $precision): Type } $arrayType = new ArrayType( - $this->getKeyType()->generalize($precision), + $this->getIterableKeyType()->generalize($precision), $this->getItemType()->generalize($precision), ); @@ -1232,12 +1281,12 @@ public function generalizeToArray(): Type return $this; } - $arrayType = new ArrayType($this->getKeyType(), $this->getItemType()); + $arrayType = new ArrayType($this->getIterableKeyType(), $this->getItemType()); if ($isIterableAtLeastOnce->yes()) { $arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); } - if ($this->isList) { + if ($this->isList->yes()) { $arrayType = AccessoryArrayListType::intersectWith($arrayType); } @@ -1249,24 +1298,7 @@ public function generalizeToArray(): Type */ 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, true); + return $this->getKeysOrValuesArray($this->keyTypes); } /** @@ -1274,24 +1306,54 @@ public function getKeysArray(): Type */ 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, true); + return new self($keyTypes, $valueTypes, $autoIndexes, $optionalKeys, TrinaryLogic::createYes()); } /** @deprecated Use getArraySize() instead */ @@ -1319,9 +1381,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); } } @@ -1376,7 +1438,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) { @@ -1415,6 +1477,32 @@ public function traverse(callable $cb): Type 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 { $keyTypesCount = count($this->keyTypes); @@ -1469,7 +1557,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) { @@ -1490,7 +1578,7 @@ public function mergeWith(self $otherArray): self $nextAutoIndexes = array_values(array_unique(array_merge($this->nextAutoIndexes, $otherArray->nextAutoIndexes))); sort($nextAutoIndexes); - return new self($this->keyTypes, $valueTypes, $nextAutoIndexes, $optionalKeys, $this->isList && $otherArray->isList); + return new self($this->keyTypes, $valueTypes, $nextAutoIndexes, $optionalKeys, $this->isList->and($otherArray->isList)); } /** @@ -1538,12 +1626,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 d27d9c378e..8547fa39a9 100644 --- a/src/Type/Constant/ConstantArrayTypeBuilder.php +++ b/src/Type/Constant/ConstantArrayTypeBuilder.php @@ -3,6 +3,7 @@ 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; @@ -18,6 +19,7 @@ use function in_array; use function is_float; use function max; +use function min; use function range; /** @api */ @@ -41,14 +43,14 @@ private function __construct( private array $valueTypes, private array $nextAutoIndexes, private array $optionalKeys, - private bool $isList, + private TrinaryLogic $isList, ) { } public static function createEmpty(): self { - return new self([], [], [0], [], true); + return new self([], [], [0], [], TrinaryLogic::createYes()); } public static function createFromConstantArray(ConstantArrayType $startArrayType): self @@ -58,7 +60,7 @@ public static function createFromConstantArray(ConstantArrayType $startArrayType $startArrayType->getValueTypes(), $startArrayType->getNextAutoIndexes(), $startArrayType->getOptionalKeys(), - $startArrayType->isList()->yes(), + $startArrayType->isList(), ); if (count($startArrayType->getKeyTypes()) > self::ARRAY_COUNT_LIMIT) { @@ -158,9 +160,14 @@ 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() !== $max) { - $this->isList = false; + 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 */ @@ -175,7 +182,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt } } } else { - $this->isList = false; + $this->isList = TrinaryLogic::createNo(); } if ($optional) { @@ -189,9 +196,9 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt return; } - $this->isList = false; + $this->isList = TrinaryLogic::createNo(); - $scalarTypes = TypeUtils::getConstantScalars($offsetType); + $scalarTypes = $offsetType->getConstantScalarTypes(); if (count($scalarTypes) === 0) { $integerRanges = TypeUtils::getIntegerRanges($offsetType); if (count($integerRanges) > 0) { @@ -252,7 +259,7 @@ 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 = false; + $this->isList = TrinaryLogic::createNo(); } $this->keyTypes[] = $offsetType; @@ -295,7 +302,7 @@ public function getArray(): Type $array = TypeCombinator::intersect($array, new OversizedArrayType()); } - if ($this->isList) { + if ($this->isList->yes()) { $array = AccessoryArrayListType::intersectWith($array); } @@ -304,7 +311,7 @@ public function getArray(): Type public function isList(): bool { - return $this->isList; + return $this->isList->yes(); } } diff --git a/src/Type/Constant/ConstantBooleanType.php b/src/Type/Constant/ConstantBooleanType.php index d71b4a795d..88b7822f6a 100644 --- a/src/Type/Constant/ConstantBooleanType.php +++ b/src/Type/Constant/ConstantBooleanType.php @@ -2,6 +2,9 @@ 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; @@ -17,7 +20,9 @@ class ConstantBooleanType extends BooleanType implements ConstantScalarType { - use ConstantScalarTypeTrait; + use ConstantScalarTypeTrait { + looseCompare as private scalarLooseCompare; + } /** @api */ public function __construct(private bool $value) @@ -112,6 +117,11 @@ public function generalize(GeneralizePrecision $precision): Type return new BooleanType(); } + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode($this->value ? 'true' : 'false'); + } + /** * @param mixed[] $properties */ @@ -120,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 867898510a..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,10 +12,11 @@ use PHPStan\Type\Traits\ConstantScalarTypeTrait; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; -use function abs; +use function ini_get; +use function ini_set; use function is_finite; -use function strpos; -use const PHP_FLOAT_EPSILON; +use function is_nan; +use function str_contains; /** @api */ class ConstantFloatType extends FloatType implements ConstantScalarType @@ -35,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 (abs($this->value - $type->value) < PHP_FLOAT_EPSILON) { - 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 @@ -95,6 +86,14 @@ 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 39f6ae4a5e..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; @@ -88,6 +91,14 @@ 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 bb77bfbdd3..51d3d49874 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\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,12 +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 @@ -75,7 +83,6 @@ public function isClassStringType(): TrinaryLogic { if ($this->isClassString) { return TrinaryLogic::createYes(); - } $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); @@ -83,6 +90,20 @@ public function isClassStringType(): TrinaryLogic 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 */ @@ -114,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\\\"") . '"'; } @@ -189,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(); } @@ -295,7 +327,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 @@ -354,6 +386,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()); @@ -474,6 +511,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/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/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 a305ac6942..4589f8e4c2 100644 --- a/src/Type/Enum/EnumCaseObjectType.php +++ b/src/Type/Enum/EnumCaseObjectType.php @@ -2,10 +2,14 @@ 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; @@ -110,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') { @@ -129,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 @@ -156,6 +184,16 @@ 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/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 f8f75a9efd..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 */ @@ -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 = []; @@ -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 c97944665b..c82a50e4e7 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -2,6 +2,9 @@ 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; @@ -51,6 +54,11 @@ public function getObjectClassNames(): array return []; } + public function getObjectClassReflections(): array + { + return []; + } + public function getConstantStrings(): array { return []; @@ -127,7 +135,7 @@ public function toArray(): Type [$this], [1], [], - true, + TrinaryLogic::createYes(), ); } @@ -141,6 +149,26 @@ 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(); @@ -196,6 +224,16 @@ 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(); @@ -206,16 +244,36 @@ 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 a0ba7a832d..fd6ecd5f03 100644 --- a/src/Type/Generic/GenericClassStringType.php +++ b/src/Type/Generic/GenericClassStringType.php @@ -2,6 +2,9 @@ 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; @@ -42,6 +45,16 @@ 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)); @@ -124,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) { @@ -180,6 +203,16 @@ 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->isClassStringType()->yes()) { diff --git a/src/Type/Generic/GenericObjectType.php b/src/Type/Generic/GenericObjectType.php index 29428dfbf1..775134aab6 100644 --- a/src/Type/Generic/GenericObjectType.php +++ b/src/Type/Generic/GenericObjectType.php @@ -2,6 +2,9 @@ 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\ExtendedMethodReflection; @@ -32,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); @@ -48,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, + )), ); } @@ -71,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; @@ -97,6 +112,12 @@ 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; @@ -168,7 +189,15 @@ private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): Accept throw new ShouldNotHappenException(); } - $results[] = $templateType->isValidVarianceWithReason($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) { @@ -194,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 @@ -264,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; } @@ -294,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; @@ -302,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), + ); } /** @@ -326,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 c1372cf9e4..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(), $bound->isList()->yes()); + 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/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/TemplateTypeArgumentStrategy.php b/src/Type/Generic/TemplateTypeArgumentStrategy.php index b61153aa36..414ffc12aa 100644 --- a/src/Type/Generic/TemplateTypeArgumentStrategy.php +++ b/src/Type/Generic/TemplateTypeArgumentStrategy.php @@ -5,6 +5,9 @@ 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 @@ -19,6 +22,17 @@ public function accepts(TemplateType $left, Type $right, bool $strictTypes): Acc } else { $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/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/TemplateTypeTrait.php b/src/Type/Generic/TemplateTypeTrait.php index 47cd2c64a7..84e6b78185 100644 --- a/src/Type/Generic/TemplateTypeTrait.php +++ b/src/Type/Generic/TemplateTypeTrait.php @@ -2,6 +2,9 @@ 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; @@ -256,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); @@ -308,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 d05c1a8a4a..a3ab7a04bd 100644 --- a/src/Type/Generic/TemplateTypeVariance.php +++ b/src/Type/Generic/TemplateTypeVariance.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Generic; +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\AcceptsResult; @@ -19,6 +20,7 @@ class TemplateTypeVariance private const COVARIANT = 2; private const CONTRAVARIANT = 3; private const STATIC = 4; + private const BIVARIANT = 5; /** @var self[] */ private static array $registry; @@ -55,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; @@ -75,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()) { @@ -84,6 +96,9 @@ public function compose(self $other): self if ($other->covariant()) { return self::createContravariant(); } + if ($other->bivariant()) { + return self::createBivariant(); + } return self::createInvariant(); } @@ -94,6 +109,9 @@ public function compose(self $other): self if ($other->covariant()) { return self::createCovariant(); } + if ($other->bivariant()) { + return self::createBivariant(); + } return self::createInvariant(); } @@ -101,6 +119,10 @@ public function compose(self $other): self return self::createInvariant(); } + if ($this->bivariant()) { + return self::createBivariant(); + } + return $other; } @@ -163,6 +185,10 @@ public function isValidVarianceWithReason(?TemplateType $templateType, Type $a, return new AcceptsResult($b->isSuperTypeOf($a), []); } + if ($this->bivariant()) { + return AcceptsResult::createYes(); + } + throw new ShouldNotHappenException(); } @@ -175,6 +201,7 @@ public function validPosition(self $other): bool { return $other->value === $this->value || $other->invariant() + || $this->bivariant() || $this->static(); } @@ -189,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(); 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/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 1f0e280aaf..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; @@ -610,6 +616,42 @@ public function exponentiate(Type $exponent): Type 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 a349494906..5e81e00519 100644 --- a/src/Type/IntegerType.php +++ b/src/Type/IntegerType.php @@ -2,6 +2,9 @@ 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; @@ -84,7 +87,7 @@ public function toArray(): Type [$this], [1], [], - true, + TrinaryLogic::createYes(), ); } @@ -128,6 +131,11 @@ 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) { @@ -149,9 +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 aac96ce51c..50073ce6eb 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -2,9 +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\ExtendedMethodReflection; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\TrivialParametersAcceptor; @@ -28,12 +34,14 @@ 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 strlen; use function substr; @@ -121,6 +129,18 @@ public function getObjectClassNames(): array 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 = []; @@ -169,6 +189,32 @@ public function acceptsWithReason(Type $otherType, bool $strictTypes): AcceptsRe $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 $result; } @@ -398,6 +444,11 @@ 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()); @@ -590,6 +641,16 @@ 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()); @@ -600,6 +661,11 @@ 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()); @@ -625,6 +691,11 @@ 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)); @@ -681,7 +752,7 @@ public function getEnumCases(): array foreach ($this->types as $type) { $oneType = []; foreach ($type->getEnumCases() as $enumCase) { - $oneType[$enumCase->describe(VerbosityLevel::typeOnly())] = $enumCase; + $oneType[md5($enumCase->describe(VerbosityLevel::typeOnly()))] = $enumCase; } $compare[] = $oneType; } @@ -726,6 +797,40 @@ 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()); @@ -884,6 +989,35 @@ 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)); @@ -894,6 +1028,26 @@ 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 */ @@ -919,4 +1073,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 ce737e9e4c..c675aff51d 100644 --- a/src/Type/IterableType.php +++ b/src/Type/IterableType.php @@ -2,10 +2,13 @@ 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\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; @@ -65,6 +68,11 @@ public function getObjectClassNames(): array return []; } + public function getObjectClassReflections(): array + { + return []; + } + public function getConstantStrings(): array { return []; @@ -182,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'; @@ -285,6 +292,26 @@ 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(); @@ -340,6 +367,16 @@ 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(); @@ -350,6 +387,11 @@ public function isScalar(): TrinaryLogic return TrinaryLogic::createNo(); } + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function getEnumCases(): array { return []; @@ -373,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), ); } @@ -391,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()); @@ -414,6 +470,38 @@ 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 0e5423ad5a..294b7d1d2b 100644 --- a/src/Type/JustNullableTypeTrait.php +++ b/src/Type/JustNullableTypeTrait.php @@ -21,6 +21,11 @@ public function getObjectClassNames(): array return []; } + public function getObjectClassReflections(): array + { + return []; + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic { return $this->acceptsWithReason($type, $strictTypes)->result; @@ -62,11 +67,36 @@ public function traverse(callable $cb): Type return $this; } + 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 isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + public function isTrue(): TrinaryLogic { return TrinaryLogic::createNo(); @@ -122,6 +152,16 @@ 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..e38c8c5cca --- /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-next-line + 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 4ef1f426ca..51f28b15ac 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -3,6 +3,9 @@ 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; @@ -68,6 +71,11 @@ public function getObjectClassNames(): array return []; } + public function getObjectClassReflections(): array + { + return []; + } + public function getArrays(): array { return []; @@ -151,6 +159,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) { @@ -330,6 +343,16 @@ public function isObject(): TrinaryLogic 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 { return TrinaryLogic::createYes(); @@ -598,6 +621,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) { @@ -657,6 +685,26 @@ public function isNull(): TrinaryLogic 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) { @@ -800,6 +848,28 @@ public function isClassStringType(): TrinaryLogic 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) { @@ -822,6 +892,11 @@ public function isScalar(): TrinaryLogic 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()) { @@ -839,6 +914,16 @@ public function exponentiate(Type $exponent): Type ]); } + 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 d06fb9a65c..3fe5b5ff5d 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -2,6 +2,9 @@ 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\ExtendedMethodReflection; @@ -61,6 +64,11 @@ public function getObjectClassNames(): array return []; } + public function getObjectClassReflections(): array + { + return []; + } + public function getConstantStrings(): array { return []; @@ -120,6 +128,11 @@ public function isObject(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isEnum(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function canAccessProperties(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -260,6 +273,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 NeverType(); @@ -363,11 +381,36 @@ public function traverse(callable $cb): Type return $this; } + 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 isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + public function isTrue(): TrinaryLogic { return TrinaryLogic::createNo(); @@ -423,6 +466,16 @@ 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(); @@ -433,6 +486,11 @@ public function isScalar(): TrinaryLogic return TrinaryLogic::createNo(); } + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + public function getEnumCases(): array { return []; @@ -443,6 +501,16 @@ 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 @@ +setOffsetValueType($offsetType, $valueType, $unionValues); } + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + public function unsetOffset(Type $offsetType): Type { return $this; @@ -192,11 +205,36 @@ public function traverse(callable $cb): Type return $this; } + 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(); @@ -252,6 +290,16 @@ 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(); @@ -262,6 +310,20 @@ 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-next-line + return new ConstantBooleanType($this->getValue() == []); // phpcs:ignore + } + + return new BooleanType(); + } + public function getSmallerType(): Type { return new NeverType(); @@ -298,6 +360,11 @@ public function getGreaterOrEqualType(): Type return new MixedType(); } + public function getFiniteTypes(): array + { + return [$this]; + } + public function exponentiate(Type $exponent): Type { return new UnionType( @@ -308,6 +375,11 @@ public function exponentiate(Type $exponent): Type ); } + 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 487f42c9df..341bb9b252 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -15,6 +15,9 @@ 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\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ConstantReflection; @@ -27,6 +30,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; @@ -46,7 +50,6 @@ use Traversable; use function array_key_exists; use function array_map; -use function array_merge; use function array_values; use function count; use function implode; @@ -121,6 +124,9 @@ private static function createFromReflection(ClassReflection $reflection): self return new GenericObjectType( $reflection->getName(), $reflection->typeMapToList($reflection->getActiveTemplateTypeMap()), + null, + null, + $reflection->varianceMapToList($reflection->getCallSiteVarianceMap()), ); } @@ -174,6 +180,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(); } @@ -235,6 +261,16 @@ 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; @@ -293,38 +329,17 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return self::$superTypes[$thisDescription][$description] = $type->isSubTypeOf($this); } - if ($type instanceof ObjectWithoutClassType) { - if ($type->getSubtractedType() !== null) { - $isSuperType = $type->getSubtractedType()->isSuperTypeOf($this); - if ($isSuperType->yes()) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createNo(); - } - } - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createMaybe(); + if ($type instanceof ClosureType) { + return self::$superTypes[$thisDescription][$description] = $this->isInstanceOf(Closure::class); } - if ($type instanceof ThisType && $type->isInTrait()) { + if ($type instanceof ObjectWithoutClassType) { if ($type->getSubtractedType() !== null) { $isSuperType = $type->getSubtractedType()->isSuperTypeOf($this); if ($isSuperType->yes()) { return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createNo(); } } - - if ($this->getClassReflection() === null) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createMaybe(); - } - - $thisClassReflection = $this->getClassReflection(); - if ($thisClassReflection->isTrait()) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createNo(); - } - - $traitReflection = $type->getTraitReflection(); - if ($thisClassReflection->isFinal() && !$thisClassReflection->hasTraitUse($traitReflection->getName())) { - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createNo(); - } - return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createMaybe(); } @@ -409,11 +424,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) { @@ -445,12 +456,12 @@ private function checkSubclassAcceptability(string $thatClass): AcceptsResult if ($thisReflection->isInterface() && $thatReflection->isInterface()) { return AcceptsResult::createFromBoolean( - $thatReflection->implementsInterface($this->className), + $thatReflection->implementsInterface($thisReflection->getName()), ); } return AcceptsResult::createFromBoolean( - $thatReflection->isSubclassOf($this->className), + $thatReflection->isSubclassOf($thisReflection->getName()), ); } @@ -659,6 +670,16 @@ 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(); @@ -844,7 +865,7 @@ 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 = $this->getTemplateType(Traversable::class, 'TKey'); if (!$tKey instanceof ErrorType) { @@ -895,7 +916,7 @@ 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 = $this->getTemplateType(Traversable::class, 'TValue'); if (!$tValue instanceof ErrorType) { @@ -937,6 +958,26 @@ 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(); @@ -992,6 +1033,16 @@ 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(); @@ -1002,6 +1053,17 @@ 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(); @@ -1118,6 +1180,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()) { @@ -1146,11 +1217,12 @@ public function getEnumCases(): array } $cases = []; + $className = $classReflection->getName(); foreach ($classReflection->getEnumCases() as $enumCase) { if (array_key_exists($enumCase->getName(), $subtracted)) { continue; } - $cases[] = new EnumCaseObjectType($classReflection->getName(), $enumCase->getName(), $classReflection); + $cases[] = new EnumCaseObjectType($className, $enumCase->getName(), $classReflection); } return $cases; @@ -1233,10 +1305,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(); } @@ -1273,10 +1353,6 @@ public function changeSubtractedType(?Type $subtractedType): Type $subtractedSubTypes = []; $subtractedTypesList = TypeUtils::flattenTypes($subtractedType); - if ($this->subtractedType !== null) { - $subtractedTypesList = array_merge($subtractedTypesList, TypeUtils::flattenTypes($this->subtractedType)); - } - $subtractedTypes = []; foreach ($subtractedTypesList as $type) { $subtractedTypes[$type->describe(VerbosityLevel::precise())] = $type; @@ -1342,6 +1418,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) { @@ -1493,6 +1578,11 @@ public function tryRemove(Type $typeToRemove): ?Type return null; } + public function getFiniteTypes(): array + { + return $this->getEnumCases(); + } + public function exponentiate(Type $exponent): Type { $object = new ObjectWithoutClassType(); @@ -1502,4 +1592,9 @@ public function exponentiate(Type $exponent): Type 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 048fca6a97..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; @@ -45,6 +47,11 @@ public function getObjectClassNames(): array return []; } + public function getObjectClassReflections(): array + { + return []; + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic { return $this->acceptsWithReason($type, $strictTypes)->result; @@ -57,7 +64,7 @@ public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult } return AcceptsResult::createFromBoolean( - $type instanceof self || $type->getObjectClassNames() !== [], + $type instanceof self || $type instanceof ObjectShapeType || $type->getObjectClassNames() !== [], ); } @@ -81,6 +88,10 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return TrinaryLogic::createMaybe(); } + if ($type instanceof ObjectShapeType) { + return TrinaryLogic::createYes(); + } + if ($type->getObjectClassNames() === []) { return TrinaryLogic::createNo(); } @@ -172,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()) { @@ -193,6 +213,16 @@ public function exponentiate(Type $exponent): Type ]); } + 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 0566730539..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 @@ -35,6 +37,11 @@ public function getObjectClassNames(): array return []; } + public function getObjectClassReflections(): array + { + return []; + } + public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array { return array_merge( @@ -52,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 @@ -82,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 65b949c60c..7299a08100 100644 --- a/src/Type/ParserNodeTypeToPHPStanType.php +++ b/src/Type/ParserNodeTypeToPHPStanType.php @@ -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 0194b11688..1c8530ec3b 100644 --- a/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php @@ -12,6 +12,7 @@ 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\NeverType; use PHPStan\Type\NullType; @@ -22,6 +23,8 @@ final class ArrayChunkFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + private const FINITE_TYPES_LIMIT = 5; + public function __construct(private PhpVersion $phpVersion) { } @@ -46,7 +49,8 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $preserveKeys = false; } - if ($lengthType instanceof ConstantIntegerType && $lengthType->getValue() < 1) { + $negativeOrZero = IntegerRangeType::fromInterval(null, 0); + if ($negativeOrZero->isSuperTypeOf($lengthType)->yes()) { return $this->phpVersion->throwsValueErrorForInternalFunctions() ? new NeverType() : new NullType(); } @@ -54,14 +58,27 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return null; } - $constantArrays = $arrayType->getConstantArrays(); - if ($lengthType instanceof ConstantIntegerType && $lengthType->getValue() >= 1 && $preserveKeys !== null && count($constantArrays) > 0) { - $results = []; - foreach ($constantArrays as $constantArray) { - $results[] = $constantArray->chunk($lengthType->getValue(), $preserveKeys); + 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); + } + } + + return TypeCombinator::union(...$results); } - - return TypeCombinator::union(...$results); } $chunkType = self::getChunkType($arrayType, $preserveKeys); diff --git a/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php b/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php index bf89efb949..a7edf8777c 100644 --- a/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php @@ -6,7 +6,6 @@ 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; @@ -18,7 +17,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 function count; @@ -35,11 +33,11 @@ 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); @@ -116,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); @@ -144,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; } 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 76fb917366..2e85f59099 100644 --- a/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php @@ -6,7 +6,6 @@ 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; @@ -35,10 +34,10 @@ 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; } $numberType = $scope->getType($functionCall->getArgs()[1]->value); diff --git a/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php b/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php index 5f3b594b36..73d0f31e36 100644 --- a/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php +++ b/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php @@ -29,6 +29,7 @@ use PHPStan\Type\StaticTypeFactory; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeUtils; use function array_map; use function count; use function is_string; @@ -54,6 +55,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $arrayArgType = $scope->getType($arrayArg); + $arrayArgType = TypeUtils::toBenevolentUnion($arrayArgType); $keyType = $arrayArgType->getIterableKeyType(); $itemType = $arrayArgType->getIterableValueType(); @@ -68,7 +70,7 @@ 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'], $arrayArgType->getArrays()), ); @@ -89,7 +91,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } } - 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) { @@ -104,7 +106,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } } - 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) { diff --git a/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php b/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php index 50c125c474..10335fa3e8 100644 --- a/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NeverType; use PHPStan\Type\NullType; @@ -27,11 +26,11 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo 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 = []; 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 f3d8b82a76..d2d5ed8360 100644 --- a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php @@ -61,7 +61,7 @@ public function specifyTypes( if (!$keyType instanceof ConstantIntegerType && !$keyType instanceof ConstantStringType && !$arrayType->isIterableAtLeastOnce()->no()) { - if ($context->truthy()) { + if ($context->true()) { $arrayKeyType = $arrayType->getIterableKeyType(); if ($keyType->isString()->yes()) { $arrayKeyType = $arrayKeyType->toString(); @@ -95,7 +95,7 @@ public function specifyTypes( 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/ArrayMapFunctionReturnTypeExtension.php b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php index 897a698dea..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) { @@ -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 9306a9263d..5bde2a454e 100644 --- a/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php @@ -5,7 +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\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; @@ -30,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 = []; 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/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 690ad91f37..80bd5866ec 100644 --- a/src/Type/Php/ArrayReduceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayReduceFunctionReturnTypeExtension.php @@ -21,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( diff --git a/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php b/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php index 8094b049ba..282c65d3b2 100644 --- a/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php @@ -5,6 +5,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; @@ -50,7 +51,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if ($valueType->isIterableAtLeastOnce()->yes()) { - return $valueType->toArray(); + 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/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 67cf40eb62..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; @@ -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/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/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 2147fd1b80..41432d1c3a 100644 --- a/src/Type/Php/DateFunctionReturnTypeExtension.php +++ b/src/Type/Php/DateFunctionReturnTypeExtension.php @@ -5,23 +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\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'; @@ -31,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 = $argType->getConstantStrings(); - - 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/DateIntervalDynamicReturnTypeExtension.php b/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php index aa9dc2c1d5..1fdcbf4937 100644 --- a/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php +++ b/src/Type/Php/DateIntervalDynamicReturnTypeExtension.php @@ -10,6 +10,7 @@ use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use Throwable; use function count; use function in_array; @@ -39,7 +40,13 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, $possibleReturnTypes = []; foreach ($strings as $string) { - $possibleReturnTypes[] = @DateInterval::createFromDateString($string->getValue()) instanceof DateInterval ? DateInterval::class : false; + 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 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/DateTimeModifyReturnTypeExtension.php b/src/Type/Php/DateTimeModifyReturnTypeExtension.php index 7fbd70ed71..17359ba45c 100644 --- a/src/Type/Php/DateTimeModifyReturnTypeExtension.php +++ b/src/Type/Php/DateTimeModifyReturnTypeExtension.php @@ -12,6 +12,7 @@ use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use Throwable; use function count; class DateTimeModifyReturnTypeExtension implements DynamicMethodReturnTypeExtension @@ -49,7 +50,15 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method $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/DefinedConstantTypeSpecifyingExtension.php b/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php index 869eedf630..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; @@ -14,14 +13,16 @@ use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\MixedType; use function count; -use function explode; -use function ltrim; class DefinedConstantTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { private TypeSpecifier $typeSpecifier; + public function __construct(private ConstantHelper $constantHelper) + { + } + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void { $this->typeSpecifier = $typeSpecifier; @@ -35,7 +36,7 @@ public function isFunctionSupported( { return $functionReflection->getName() === 'defined' && count($node->getArgs()) >= 1 - && $context->truthy(); + && $context->true(); } public function specifyTypes( @@ -53,24 +54,8 @@ public function specifyTypes( return new SpecifiedTypes([], []); } - $classConstParts = explode('::', $constantName->getValue()); - if (count($classConstParts) >= 2) { - $classConstName = new Node\Name\FullyQualified(ltrim($classConstParts[0], '\\')); - if ($classConstName->isSpecialClassName()) { - $classConstName = new Node\Name($classConstName->toString()); - } - $constNode = new Node\Expr\ClassConstFetch( - $classConstName, - new Node\Identifier($classConstParts[1]), - ); - } else { - $constNode = new Node\Expr\ConstFetch( - new Node\Name\FullyQualified($constantName->getValue()), - ); - } - return $this->typeSpecifier->create( - $constNode, + $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 396021995a..498904c528 100644 --- a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php @@ -39,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); diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index 579c54f901..a2ed6c792b 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\Php; use PhpParser\Node; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ReflectionProvider; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; @@ -19,6 +20,7 @@ use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; +use PHPStan\Type\NeverType; use PHPStan\Type\NullType; use PHPStan\Type\StringType; use PHPStan\Type\Type; @@ -46,12 +48,59 @@ final class FilterFunctionReturnTypeHelper /** @var array>|null */ private ?array $filterTypeOptions = null; - public function __construct(private ReflectionProvider $reflectionProvider) + private ?Type $supportedFilterInputTypes = null; + + public function __construct(private ReflectionProvider $reflectionProvider, private PhpVersion $phpVersion) { $this->flagsString = new ConstantStringType('flags'); } - public function getTypeFromFunctionCall(Type $inputType, ?Type $filterType, ?Type $flagsType): Type + 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(); @@ -75,6 +124,18 @@ public function getTypeFromFunctionCall(Type $inputType, ?Type $filterType, ?Typ ? 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; } @@ -93,14 +154,18 @@ public function getTypeFromFunctionCall(Type $inputType, ?Type $filterType, ?Typ $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 ($this->hasFlag($this->getConstant('FILTER_FORCE_ARRAY'), $flagsType)) { - return new ArrayType(new MixedType(), $type); + if (!$hasRequireArrayFlag && $hasForceArrayFlag) { + return new ArrayType($inputArrayKeyType ?? $mixedType, $type); } return $type; @@ -119,6 +184,7 @@ private function getFilterTypeMap(): array $floatType = new FloatType(); $intType = new IntegerType(); $stringType = new StringType(); + $nonFalsyStringType = TypeCombinator::intersect($stringType, new AccessoryNonFalsyStringType()); $this->filterTypeMap = [ $this->getConstant('FILTER_UNSAFE_RAW') => $stringType, @@ -131,13 +197,13 @@ private function getFilterTypeMap(): array $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_EMAIL') => $nonFalsyStringType, $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_IP') => $nonFalsyStringType, + $this->getConstant('FILTER_VALIDATE_MAC') => $nonFalsyStringType, $this->getConstant('FILTER_VALIDATE_REGEXP') => $stringType, - $this->getConstant('FILTER_VALIDATE_URL') => $stringType, + $this->getConstant('FILTER_VALIDATE_URL') => $nonFalsyStringType, ]; if ($this->reflectionProvider->hasConstant(new Node\Name('FILTER_SANITIZE_MAGIC_QUOTES'), null)) { @@ -186,6 +252,10 @@ private function determineExactType(Type $in, int $filterValue, Type $defaultTyp if ($in->isBoolean()->yes()) { return $in; } + + if ($in->isNull()->yes()) { + return $defaultType; + } } if ($filterValue === $this->getConstant('FILTER_VALIDATE_FLOAT')) { @@ -196,6 +266,14 @@ private function determineExactType(Type $in, int $filterValue, Type $defaultTyp 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')) { @@ -203,6 +281,14 @@ private function determineExactType(Type $in, int $filterValue, Type $defaultTyp 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() 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 3042f5b23e..438d6440e9 100644 --- a/src/Type/Php/FilterVarDynamicReturnTypeExtension.php +++ b/src/Type/Php/FilterVarDynamicReturnTypeExtension.php @@ -32,7 +32,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $filterType = isset($functionCall->getArgs()[1]) ? $scope->getType($functionCall->getArgs()[1]->value) : null; $flagsType = isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : null; - return $this->filterFunctionReturnTypeHelper->getTypeFromFunctionCall($inputType, $filterType, $flagsType); + 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 fa51bae994..a6c2fdfdb5 100644 --- a/src/Type/Php/GetClassDynamicReturnTypeExtension.php +++ b/src/Type/Php/GetClassDynamicReturnTypeExtension.php @@ -17,9 +17,9 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StaticType; -use PHPStan\Type\ThisType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; +use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use function count; @@ -49,7 +49,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $argType = $scope->getType($args[0]->value); - if ($scope->isInTrait() && $argType instanceof ThisType) { + if ($scope->isInTrait() && TypeUtils::findThisType($argType) !== null) { return new ClassStringType(); } diff --git a/src/Type/Php/GetParentClassDynamicFunctionReturnTypeExtension.php b/src/Type/Php/GetParentClassDynamicFunctionReturnTypeExtension.php index dbacb99f54..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,7 +53,7 @@ public function getTypeFromFunctionCall( $argType = $scope->getType($functionCall->getArgs()[0]->value); if ($scope->isInTrait() && TypeUtils::findThisType($argType) !== null) { - return $defaultReturnType; + return null; } $constantStrings = $argType->getConstantStrings(); @@ -70,7 +66,7 @@ public function getTypeFromFunctionCall( 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/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/InArrayFunctionTypeSpecifyingExtension.php b/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php index b93611f593..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($arrayValueType->getEnumCases()) > 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($needleType->getEnumCases()) > 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 23212393ec..0eb26199df 100644 --- a/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php @@ -39,7 +39,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n } $classType = $scope->getType($node->getArgs()[1]->value); - if (!$classType instanceof ConstantStringType && !$context->truthy()) { + if (!$classType instanceof ConstantStringType && !$context->true()) { return new SpecifiedTypes([], []); } 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 81736b1a8e..3eba789175 100644 --- a/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php +++ b/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.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\AccessoryArrayListType; use PHPStan\Type\ArrayType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -21,12 +20,12 @@ 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); 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/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/MbStrlenFunctionReturnTypeExtension.php b/src/Type/Php/MbStrlenFunctionReturnTypeExtension.php index 873842f5e6..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 = []; @@ -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 = []; @@ -137,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 647f700e67..08a366a59b 100644 --- a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php @@ -35,7 +35,7 @@ public function isFunctionSupported( ): bool { return $functionReflection->getName() === 'method_exists' - && $context->truthy() + && $context->true() && count($node->getArgs()) >= 2; } 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 cd0943619f..e1633c0b64 100644 --- a/src/Type/Php/MinMaxFunctionReturnTypeExtension.php +++ b/src/Type/Php/MinMaxFunctionReturnTypeExtension.php @@ -8,7 +8,6 @@ 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; @@ -19,16 +18,11 @@ 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, ) @@ -37,13 +31,13 @@ public function __construct( 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) { 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 866c8abc7b..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,12 +44,10 @@ 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(); @@ -59,14 +55,14 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, 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); diff --git a/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php b/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php index b89b8a32be..2139200e0b 100644 --- a/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php @@ -51,15 +51,15 @@ public function getTypeFromFunctionCall( if ($argsCount === 1) { return $arrayType; - } else { - $flagsType = $scope->getType($functionCall->getArgs()[1]->value); - if ($flagsType instanceof ConstantIntegerType) { - if ($flagsType->getValue() === $this->getConstant('PATHINFO_ALL')) { - return $arrayType; - } - - return 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()); diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index ad3e1c3432..6300d6887d 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -5,7 +5,7 @@ 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; @@ -34,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(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 449fda7e86..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; } diff --git a/src/Type/Php/RandomIntFunctionReturnTypeExtension.php b/src/Type/Php/RandomIntFunctionReturnTypeExtension.php index cbd6f3b396..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; @@ -26,14 +25,14 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo 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 (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 9857975ca3..9d48e74c01 100644 --- a/src/Type/Php/RangeFunctionReturnTypeExtension.php +++ b/src/Type/Php/RangeFunctionReturnTypeExtension.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\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; @@ -22,7 +21,6 @@ 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; @@ -110,15 +108,17 @@ 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) { + 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) { + if ($argType->isFloat()->yes()) { return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new FloatType())); } @@ -129,8 +129,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $numberType)); } - $isString = $argType->isString()->yes(); - if ($isString) { + if ($argType->isString()->yes()) { return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new StringType())); } 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/ReplaceFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php index 53e125382a..1da17b48d8 100644 --- a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php @@ -21,8 +21,7 @@ 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, @@ -32,8 +31,7 @@ class ReplaceFunctionsDynamicReturnTypeExtension implements DynamicFunctionRetur 'strtr' => 0, ]; - /** @var array */ - private array $functionsReplacePosition = [ + private const FUNCTIONS_REPLACE_POSITION = [ 'preg_replace' => 1, 'str_replace' => 1, 'str_ireplace' => 1, @@ -43,7 +41,7 @@ class ReplaceFunctionsDynamicReturnTypeExtension implements DynamicFunctionRetur 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( @@ -75,7 +73,7 @@ private function getPreliminarilyResolvedTypeFromFunctionCall( Scope $scope, ): Type { - $argumentPosition = $this->functionsSubjectPosition[$functionReflection->getName()]; + $argumentPosition = self::FUNCTIONS_SUBJECT_POSITION[$functionReflection->getName()]; $defaultReturnType = ParametersAcceptorSelector::selectFromArgs( $scope, $functionCall->getArgs(), @@ -91,8 +89,8 @@ private function getPreliminarilyResolvedTypeFromFunctionCall( 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); 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 4d2d2906e6..41d903e5c5 100644 --- a/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php +++ b/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php @@ -11,13 +11,16 @@ use PHPStan\Type\TypeCombinator; use SimpleXMLElement; use function count; +use function extension_loaded; 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 diff --git a/src/Type/Php/SimpleXMLElementXpathMethodReturnTypeExtension.php b/src/Type/Php/SimpleXMLElementXpathMethodReturnTypeExtension.php index d9a7fefa34..5ce6d57598 100644 --- a/src/Type/Php/SimpleXMLElementXpathMethodReturnTypeExtension.php +++ b/src/Type/Php/SimpleXMLElementXpathMethodReturnTypeExtension.php @@ -5,7 +5,6 @@ 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; @@ -13,6 +12,7 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use SimpleXMLElement; +use function extension_loaded; class SimpleXMLElementXpathMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension { @@ -24,13 +24,13 @@ 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); @@ -41,14 +41,14 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method $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 12413a49f5..ef70f072b9 100644 --- a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php @@ -4,16 +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\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; @@ -40,7 +41,7 @@ public function getTypeFromFunctionCall( { $args = $functionCall->getArgs(); if (count($args) === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $formatType = $scope->getType($args[0]->value); @@ -84,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; } - } catch (Throwable) { + + try { + if ($functionReflection->getName() === 'sprintf') { + $returnTypes[] = $scope->getTypeFromValue(@sprintf($format, ...$combination)); + } else { + $returnTypes[] = $scope->getTypeFromValue(@vsprintf($format, $combination)); + } + } catch (Throwable) { + return $returnType; + } + } + + 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 411bb63e7d..b077807981 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; @@ -50,18 +48,18 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope, - ): Type + ): ?Type { $fnName = $functionReflection->getName(); $args = $functionCall->getArgs(); if (count($args) < self::FUNCTIONS[$fnName]) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } $argType = $scope->getType($args[0]->value); if (!is_callable($fnName)) { - throw new ShouldNotHappenException(); + return null; } $modes = []; 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/StrSplitFunctionReturnTypeExtension.php b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php index 2cc1b3a98b..34d58152dc 100644 --- a/src/Type/Php/StrSplitFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php @@ -6,8 +6,8 @@ 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; @@ -20,6 +20,7 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function array_is_list; use function array_map; use function array_unique; use function count; @@ -42,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) { @@ -69,7 +68,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $values = array_unique(array_map(static fn (ConstantStringType $encoding): string => $encoding->getValue(), $strings)); if (count($values) !== 1) { - return $defaultReturnType; + return null; } $encoding = $values[0]; @@ -82,7 +81,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if (!isset($splitLength)) { - return $defaultReturnType; + return null; } $stringType = $scope->getType($functionCall->getArgs()[0]->value); @@ -134,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/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 7322564e57..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; @@ -37,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) { 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 51668a9883..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; @@ -49,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( @@ -60,7 +59,7 @@ public function getTypeFromFunctionCall( $functionCall, ); if ($isAlways === null) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return null; } return new ConstantBooleanType($isAlways); diff --git a/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php b/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php index bb03ba22fa..b8a77540cb 100644 --- a/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.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; @@ -28,10 +27,10 @@ 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 = $scope->getType($functionCall->getArgs()[0]->value)->getConstantStrings(); diff --git a/src/Type/ResourceType.php b/src/Type/ResourceType.php index 4dbd2e3a78..0eee8cea5f 100644 --- a/src/Type/ResourceType.php +++ b/src/Type/ResourceType.php @@ -2,6 +2,9 @@ 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; @@ -74,7 +77,7 @@ public function toArray(): Type [$this], [1], [], - true, + TrinaryLogic::createYes(), ); } @@ -88,11 +91,26 @@ 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 98f1ae7a97..5e11606948 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -2,6 +2,9 @@ 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; @@ -82,10 +85,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), ); } @@ -108,6 +114,11 @@ public function getObjectClassNames(): array return $this->getStaticObjectType()->getObjectClassNames(); } + public function getObjectClassReflections(): array + { + return $this->getStaticObjectType()->getObjectClassReflections(); + } + public function getArrays(): array { return $this->getStaticObjectType()->getArrays(); @@ -143,13 +154,6 @@ public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult 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); } @@ -159,7 +163,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) { @@ -175,8 +187,6 @@ public function equals(Type $type): bool return false; } - /** @var StaticType $type */ - $type = $type; return $this->getStaticObjectType()->equals($type->getStaticObjectType()); } @@ -195,6 +205,11 @@ public function isObject(): TrinaryLogic return $this->getStaticObjectType()->isObject(); } + public function isEnum(): TrinaryLogic + { + return $this->getStaticObjectType()->isEnum(); + } + public function canAccessProperties(): TrinaryLogic { return $this->getStaticObjectType()->canAccessProperties(); @@ -376,6 +391,11 @@ 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); @@ -461,6 +481,26 @@ 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(); @@ -516,6 +556,16 @@ 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(); @@ -526,6 +576,11 @@ public function isScalar(): TrinaryLogic return $this->getStaticObjectType()->isScalar(); } + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + return new BooleanType(); + } + /** * @return ParametersAcceptor[] */ @@ -588,6 +643,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) { @@ -642,6 +706,16 @@ 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 4bf38637ab..2cb696d9f5 100644 --- a/src/Type/StrictMixedType.php +++ b/src/Type/StrictMixedType.php @@ -2,6 +2,9 @@ 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\ExtendedMethodReflection; @@ -38,6 +41,11 @@ public function getObjectClassNames(): array return []; } + public function getObjectClassReflections(): array + { + return []; + } + public function getConstantStrings(): array { return []; @@ -94,7 +102,12 @@ 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 @@ -107,6 +120,11 @@ public function isObject(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isEnum(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function canAccessProperties(): TrinaryLogic { return TrinaryLogic::createNo(); @@ -187,6 +205,26 @@ 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(); @@ -242,6 +280,16 @@ 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(); @@ -252,6 +300,11 @@ 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(); @@ -272,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(); @@ -347,11 +405,26 @@ 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 503b4fb4cf..0130d7e094 100644 --- a/src/Type/StringAlwaysAcceptingObjectWithToStringType.php +++ b/src/Type/StringAlwaysAcceptingObjectWithToStringType.php @@ -3,10 +3,36 @@ namespace PHPStan\Type; use PHPStan\Reflection\ReflectionProviderStaticAccessor; +use PHPStan\TrinaryLogic; class StringAlwaysAcceptingObjectWithToStringType extends StringType { + public function isSuperTypeOf(Type $type): TrinaryLogic + { + 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($thatClassName); + $result = $result->or(TrinaryLogic::createFromBoolean($typeClass->hasNativeMethod('__toString'))); + } + + return $result; + } + public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { $thatClassNames = $type->getObjectClassNames(); diff --git a/src/Type/StringType.php b/src/Type/StringType.php index 5eb3e69107..c9972ee053 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -2,6 +2,9 @@ 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; @@ -55,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 @@ -78,13 +81,18 @@ 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(); @@ -152,7 +160,7 @@ public function toArray(): Type [$this], [1], [], - true, + TrinaryLogic::createYes(), ); } @@ -221,11 +229,26 @@ 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()) { @@ -247,11 +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 441ebd2a53..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; @@ -17,7 +19,6 @@ class ThisType extends StaticType public function __construct( ClassReflection $classReflection, ?Type $subtractedType = null, - private ?ClassReflection $traitReflection = null, ) { parent::__construct($classReflection, $subtractedType); @@ -58,19 +59,6 @@ public function changeSubtractedType(?Type $subtractedType): Type return $type; } - /** - * @phpstan-assert-if-true !null $this->getTraitReflection() - */ - public function isInTrait(): bool - { - return $this->traitReflection !== null; - } - - public function getTraitReflection(): ?ClassReflection - { - return $this->traitReflection; - } - public function traverse(callable $cb): Type { $subtractedType = $this->getSubtractedType() !== null ? $cb($this->getSubtractedType()) : null; @@ -85,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 5dfe630a5b..584bc5f153 100644 --- a/src/Type/Traits/ConstantScalarTypeTrait.php +++ b/src/Type/Traits/ConstantScalarTypeTrait.php @@ -2,10 +2,15 @@ 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 @@ -19,7 +24,7 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof self) { - return AcceptsResult::createFromBoolean($this->value === $type->value); + return AcceptsResult::createFromBoolean($this->equals($type)); } if ($type instanceof CompoundType) { @@ -32,7 +37,7 @@ public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult 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) { @@ -46,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-next-line + 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; @@ -77,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 9bb63542fd..294726f2d9 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Traits; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\ExtendedMethodReflection; @@ -27,6 +28,11 @@ public function getObjectClassNames(): array return $this->resolve()->getObjectClassNames(); } + public function getObjectClassReflections(): array + { + return $this->resolve()->getObjectClassReflections(); + } + public function getArrays(): array { return $this->resolve()->getArrays(); @@ -86,6 +92,11 @@ public function isObject(): TrinaryLogic return $this->resolve()->isObject(); } + public function isEnum(): TrinaryLogic + { + return $this->resolve()->isEnum(); + } + public function canAccessProperties(): TrinaryLogic { return $this->resolve()->canAccessProperties(); @@ -226,6 +237,11 @@ 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); @@ -346,6 +362,26 @@ 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(); @@ -401,6 +437,16 @@ 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(); @@ -411,6 +457,11 @@ 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(); @@ -495,6 +546,11 @@ 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/MaybeObjectTypeTrait.php b/src/Type/Traits/MaybeObjectTypeTrait.php index 7ebee70b27..b8097947b4 100644 --- a/src/Type/Traits/MaybeObjectTypeTrait.php +++ b/src/Type/Traits/MaybeObjectTypeTrait.php @@ -30,6 +30,11 @@ public function isObject(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isEnum(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function canAccessProperties(): TrinaryLogic { return TrinaryLogic::createMaybe(); 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/NonObjectTypeTrait.php b/src/Type/Traits/NonObjectTypeTrait.php index 7e7395cd91..3cba4b5bca 100644 --- a/src/Type/Traits/NonObjectTypeTrait.php +++ b/src/Type/Traits/NonObjectTypeTrait.php @@ -21,6 +21,11 @@ public function isObject(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isEnum(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function canAccessProperties(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Traits/NonOffsetAccessibleTypeTrait.php b/src/Type/Traits/NonOffsetAccessibleTypeTrait.php index 929d8c0002..d74dc8d0d1 100644 --- a/src/Type/Traits/NonOffsetAccessibleTypeTrait.php +++ b/src/Type/Traits/NonOffsetAccessibleTypeTrait.php @@ -29,6 +29,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(); diff --git a/src/Type/Traits/ObjectTypeTrait.php b/src/Type/Traits/ObjectTypeTrait.php index 611f37b32c..8105cd0555 100644 --- a/src/Type/Traits/ObjectTypeTrait.php +++ b/src/Type/Traits/ObjectTypeTrait.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Traits; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\Dummy\DummyConstantReflection; @@ -15,6 +16,7 @@ use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\ArrayType; +use PHPStan\Type\BooleanType; use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; use PHPStan\Type\StringType; @@ -39,6 +41,11 @@ public function isObject(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isEnum(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function canAccessProperties(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -121,6 +128,26 @@ 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(); @@ -176,6 +203,16 @@ 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(); @@ -186,6 +223,11 @@ 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(); diff --git a/src/Type/Type.php b/src/Type/Type.php index 91a1ab6489..7353fb91e1 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -2,7 +2,10 @@ namespace PHPStan\Type; +use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ClassMemberAccessAnswerer; +use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\ParametersAcceptor; @@ -17,7 +20,10 @@ use PHPStan\Type\Generic\TemplateTypeReference; use PHPStan\Type\Generic\TemplateTypeVariance; -/** @api */ +/** + * @api + * @see https://phpstan.org/developing-extensions/type-system + */ interface Type { @@ -29,8 +35,26 @@ 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; @@ -113,6 +137,8 @@ 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; @@ -138,6 +164,22 @@ public function shuffleArray(): Type; */ 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; @@ -167,6 +209,26 @@ 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; @@ -195,6 +257,8 @@ public function isVoid(): TrinaryLogic; public function isScalar(): TrinaryLogic; + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType; + public function getSmallerType(): Type; public function getSmallerOrEqualType(): Type; @@ -257,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 df82042215..5159711e6f 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -7,6 +7,7 @@ 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; @@ -15,7 +16,9 @@ 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; @@ -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 @@ -144,6 +181,7 @@ public static function union(Type ...$types): Type $arrayTypes = []; $scalarTypes = []; $hasGenericScalarTypes = []; + $enumCaseTypes = []; for ($i = 0; $i < $typesCount; $i++) { if ($types[$i] instanceof ConstantScalarType) { $type = $types[$i]; @@ -163,6 +201,13 @@ public static function union(Type ...$types): Type if ($types[$i] instanceof StringType && !$types[$i] instanceof ClassStringType) { $hasGenericScalarTypes[ConstantStringType::class] = true; } + if ($types[$i] instanceof EnumCaseObjectType) { + $enumCaseTypes[$types[$i]->describe(VerbosityLevel::cache())] = $types[$i]; + + unset($types[$i]); + continue; + } + if (!$types[$i]->isArray()->yes()) { continue; } @@ -175,12 +220,8 @@ public static function union(Type ...$types): Type $scalarTypes[$classType] = array_values($scalarTypeItems); } - $types = array_values( - array_merge( - $types, - self::processArrayTypes($arrayTypes), - ), - ); + $enumCaseTypes = array_values($enumCaseTypes); + $types = array_values($types); $typesCount = count($types); foreach ($scalarTypes as $classType => $scalarTypeItems) { @@ -228,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++) { @@ -256,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; @@ -319,7 +394,7 @@ 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; } @@ -568,7 +643,7 @@ private static function processArrayAccessoryTypes(array $arrayTypes): array } /** - * @param Type[] $arrayTypes + * @param list $arrayTypes * @return Type[] */ private static function processArrayTypes(array $arrayTypes): array @@ -589,14 +664,26 @@ private static function processArrayTypes(array $arrayTypes): array $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 $arrayIdx => $arrayType) { + $constantArrays = $constantArraysMap[$arrayIdx]; + $isConstantArray = $constantArrays !== []; + if (!$isConstantArray || !$arrayType->isIterableAtLeastOnce()->no()) { + $filledArrays++; + } - foreach ($arrayTypes as $arrayType) { - if ($generalArrayOccurred || !$arrayType->isConstantArray()->yes()) { + if ($generalArrayOccurred || !$isConstantArray) { foreach ($arrayType->getArrays() as $type) { - $keyTypesForGeneralArray[] = $type->getKeyType(); + $keyTypesForGeneralArray[] = $type->getIterableKeyType(); $valueTypesForGeneralArray[] = $type->getItemType(); $generalArrayOccurred = true; } @@ -618,22 +705,51 @@ private static function processArrayTypes(array $arrayTypes): array $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(...self::optimizeConstantArrays($valueTypesForGeneralArray)), - ), ...$accessoryTypes), + self::intersect($arrayType, ...$accessoryTypes), ]; } - $reducedArrayTypes = self::reduceArrays($arrayTypes); + $reducedArrayTypes = self::reduceArrays($arrayTypes, true); return array_map( static fn (Type $arrayType) => self::intersect($arrayType, ...$accessoryTypes), @@ -726,16 +842,21 @@ private static function countConstantArrayValueTypes(array $types): int } /** - * @param Type[] $constantArrays - * @return Type[] + * @param list $constantArrays + * @return list */ - private static function reduceArrays(array $constantArrays): array + private static function reduceArrays(array $constantArrays, bool $preserveTaggedUnions): array { $newArrays = []; $arraysToProcess = []; $emptyArray = null; foreach ($constantArrays as $constantArray) { 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; } @@ -781,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]); @@ -790,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; + } } } @@ -904,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); } @@ -1051,6 +1185,20 @@ public static function intersect(Type ...$types): Type continue 2; } + 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(); @@ -1087,7 +1235,7 @@ public static function intersect(Type ...$types): Type ($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); diff --git a/src/Type/TypeUtils.php b/src/Type/TypeUtils.php index 88f4f370f8..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; @@ -110,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 @@ -118,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 @@ -177,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 @@ -272,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[] */ @@ -382,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 2663a784ea..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 { @@ -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; @@ -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 ca255bee67..7917a7ec20 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -5,9 +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\ExtendedMethodReflection; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\Type\UnionTypeUnresolvedMethodPrototypeReflection; @@ -16,7 +20,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,13 +27,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 @@ -87,7 +95,7 @@ public function isNormalized(): bool /** * @return Type[] */ - private function getSortedTypes(): array + protected function getSortedTypes(): array { if ($this->sortedTypes) { return $this->types; @@ -116,22 +124,42 @@ public function getReferencedClasses(): array public function getObjectClassNames(): array { - return array_values(array_unique($this->pickFromTypes(static fn (Type $type) => $type->getObjectClassNames()))); + 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()); + 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()); + 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()); + return $this->pickFromTypes( + static fn (Type $type) => $type->getConstantStrings(), + static fn (Type $type) => $type->isString()->yes(), + ); } public function accepts(Type $type, bool $strictTypes): TrinaryLogic @@ -167,6 +195,13 @@ public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult 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; } @@ -265,7 +300,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; @@ -275,6 +310,26 @@ 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); }; @@ -282,8 +337,8 @@ public function describe(VerbosityLevel $level): string 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()); } @@ -366,6 +421,11 @@ 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()); @@ -558,6 +618,16 @@ 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()); @@ -568,6 +638,11 @@ 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()); @@ -602,6 +677,11 @@ 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)); @@ -654,7 +734,10 @@ public function shuffleArray(): Type public function getEnumCases(): array { - return $this->pickFromTypes(static fn (Type $type) => $type->getEnumCases()); + return $this->pickFromTypes( + static fn (Type $type) => $type->getEnumCases(), + static fn (Type $type) => $type->isObject()->yes(), + ); } public function isCallable(): TrinaryLogic @@ -667,15 +750,21 @@ public function isCallable(): TrinaryLogic */ 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 @@ -698,6 +787,26 @@ 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()); @@ -887,6 +996,35 @@ 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)); @@ -897,6 +1035,21 @@ 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 */ @@ -938,7 +1091,38 @@ protected function unionTypes(callable $getType): Type */ protected function pickTypes(callable $getTypes): array { - return $this->pickFromTypes($getTypes); + 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())); } /** @@ -946,7 +1130,7 @@ protected function pickTypes(callable $getTypes): array * @param callable(Type $type): list $getValues * @return list */ - protected function pickFromTypes(callable $getValues): array + private function notBenevolentPickFromTypes(callable $getValues): array { $values = []; foreach ($this->types as $type) { 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 5efcf17837..c80bcbce99 100644 --- a/src/Type/VerbosityLevel.php +++ b/src/Type/VerbosityLevel.php @@ -78,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 { @@ -86,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; } diff --git a/src/Type/VoidType.php b/src/Type/VoidType.php index 2e7d377b14..07f755ce16 100644 --- a/src/Type/VoidType.php +++ b/src/Type/VoidType.php @@ -2,6 +2,9 @@ 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; @@ -47,6 +50,11 @@ public function getObjectClassNames(): array return []; } + public function getObjectClassReflections(): array + { + return []; + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic { return $this->acceptsWithReason($type, $strictTypes)->result; @@ -58,7 +66,7 @@ public function acceptsWithReason(Type $type, bool $strictTypes): AcceptsResult return $type->isAcceptedWithReasonBy($this, $strictTypes); } - return AcceptsResult::createFromBoolean($type instanceof self); + return new AcceptsResult($type->isVoid()->or($type->isNull()), []); } public function isSuperTypeOf(Type $type): TrinaryLogic @@ -119,6 +127,26 @@ 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(); @@ -174,6 +202,16 @@ 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(); @@ -184,16 +222,36 @@ 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/ReflectionClass.stub b/stubs/ReflectionClass.stub index c114da9b7a..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 { 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 17910633ab..3f7c44f120 100644 --- a/stubs/SplObjectStorage.stub +++ b/stubs/SplObjectStorage.stub @@ -42,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 5e4f5af644..25c73249ec 100644 --- a/stubs/arrayFunctions.stub +++ b/stubs/arrayFunctions.stub @@ -23,7 +23,6 @@ function array_reduce( * * @param TArray $array * @param callable(T,T):int $callback - * @param-out (TArray is non-empty-array ? non-empty-array : array) $array */ function uasort(array &$array, callable $callback): bool {} @@ -34,7 +33,6 @@ function uasort(array &$array, callable $callback): bool * * @param TArray $array * @param callable(T,T):int $callback - * @param-out (TArray is non-empty-array ? non-empty-list : list) $array */ function usort(array &$array, callable $callback): bool {} @@ -46,7 +44,6 @@ function usort(array &$array, callable $callback): bool * * @param TArray $array * @param callable(TKey,TKey):int $callback - * @param-out (TArray is non-empty-array ? non-empty-array : array) $array */ function uksort(array &$array, callable $callback): bool { @@ -57,7 +54,7 @@ function uksort(array &$array, callable $callback): bool * * @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, @@ -67,6 +64,6 @@ function array_udiff( /** * @param array $value - * @return ($value is list ? true : false) + * @return ($value is __always-list ? true : false) */ function array_is_list(array $value): bool {} diff --git a/stubs/core.stub b/stubs/core.stub index 1aa12a1d40..c38bd53226 100644 --- a/stubs/core.stub +++ b/stubs/core.stub @@ -68,6 +68,11 @@ function base64_encode(string $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 $result @@ -114,7 +119,6 @@ function passthru(string $command, &$result_code): ?bool {} * @template TArray as array * * @param TArray $array - * @param-out (TArray is non-empty-array ? non-empty-list : list) $array */ function shuffle(array &$array): bool { @@ -125,7 +129,6 @@ function shuffle(array &$array): bool * @template TArray as array * * @param TArray $array - * @param-out (TArray is non-empty-array ? non-empty-list : list) $array */ function sort(array &$array, int $flags = SORT_REGULAR): bool { @@ -136,7 +139,6 @@ function sort(array &$array, int $flags = SORT_REGULAR): bool * @template TArray as array * * @param TArray $array - * @param-out (TArray is non-empty-array ? non-empty-list : list) $array */ function rsort(array &$array, int $flags = SORT_REGULAR): bool { 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/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index e8b575281d..fb181304bd 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; @@ -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'); @@ -682,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 @@ -773,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()); @@ -1015,6 +1023,30 @@ 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'); @@ -1130,6 +1162,161 @@ public function testSkipCheckNoGenericClasses(): void $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 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[] @@ -1137,13 +1324,11 @@ public function testSkipCheckNoGenericClasses(): 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); $errors = $analyser->analyse([$file], null, null, true, $allAnalysedFiles)->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 f45e9711a5..430e85fb56 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -16,11 +16,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; @@ -463,30 +465,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()); + } } /** @@ -575,9 +591,10 @@ private function runAnalyser( bool $reportUnmatchedIgnoredErrors, $filePaths, bool $onlyFiles, + bool $enableIgnoreErrorsWithinPhpDocs = true, ): array { - $analyser = $this->createAnalyser($reportUnmatchedIgnoredErrors); + $analyser = $this->createAnalyser($reportUnmatchedIgnoredErrors, $enableIgnoreErrorsWithinPhpDocs); if (is_string($filePaths)) { $filePaths = [$filePaths]; @@ -608,7 +625,7 @@ private function runAnalyser( ); } - private function createAnalyser(bool $reportUnmatchedIgnoredErrors): Analyser + private function createAnalyser(bool $reportUnmatchedIgnoredErrors, bool $enableIgnoreErrorsWithinPhpDocs): Analyser { $ruleRegistry = new DirectRuleRegistry([ new AlwaysFailRule(), @@ -626,22 +643,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( @@ -652,6 +674,7 @@ private function createAnalyser(bool $reportUnmatchedIgnoredErrors): Analyser $lexer, new NameResolver(), self::getContainer(), + $enableIgnoreErrorsWithinPhpDocs, ), new DependencyResolver($fileHelper, $reflectionProvider, new ExportedNodeResolver($fileTypeMapper, new ExprPrinter(new Printer())), $fileTypeMapper), new RuleErrorTransformer(), diff --git a/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php index 83e633f387..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[] @@ -178,4 +212,17 @@ private function runAnalyse(array $files): array 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/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/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/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/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 433acb53e6..e31670f0e8 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; } @@ -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', ], [ @@ -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', ], [ @@ -3865,11 +3865,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( @@ -3983,7 +3983,7 @@ public function testNotSwitchInstanceof(): void { $this->assertTypes( __DIR__ . '/data/switch-instanceof-not.php', - '*ERROR*', + '*NEVER*', '$foo', ); } @@ -4359,7 +4359,7 @@ public function dataForeachArrayType(): array ], [ __DIR__ . '/data/foreach/foreach-iterable-with-specified-key-type.php', - 'ForeachWithGenericsPhpDoc\Bar|ForeachWithGenericsPhpDoc\Foo', + 'ForeachWithGenericsPhpDocIterable\Bar|ForeachWithGenericsPhpDocIterable\Foo', '$key', ], [ @@ -4369,7 +4369,7 @@ public function dataForeachArrayType(): array ], [ __DIR__ . '/data/foreach/foreach-iterable-with-complex-value-type.php', - 'float|ForeachWithComplexValueType\Foo', + 'float|ForeachIterableWithComplexValueType\Foo', '$value', ], [ @@ -4555,7 +4555,7 @@ public function dataArrayFunctions(): array 'array_combine([1], [2])', ], [ - 'false', + PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*', 'array_combine([1, 2], [3])', ], [ @@ -5333,7 +5333,7 @@ public function dataFunctions(): array '$mbInternalEncodingWithUnknownEncoding', ], [ - 'list', + 'list', '$mbEncodingAliasesWithValidEncoding', ], [ @@ -5341,11 +5341,11 @@ public function dataFunctions(): array '$mbEncodingAliasesWithInvalidEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'list|false' : 'list', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbEncodingAliasesWithValidAndInvalidEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'list|false' : 'list', + PHP_VERSION_ID < 80000 ? 'list|false' : 'list', '$mbEncodingAliasesWithUnknownEncoding', ], [ @@ -5652,7 +5652,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)', ], [ @@ -6072,15 +6072,15 @@ public function dataVoid(): array { return [ [ - 'void', + 'null', '$this->doFoo()', ], [ - 'void', + 'null', '$this->doBar()', ], [ - 'void', + 'null', '$this->doConflictingVoid()', ], ]; @@ -7462,11 +7462,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'], @@ -7963,7 +7965,7 @@ public function dataPassedByReference(): array '$matches', ], [ - 'mixed', + 'string', '$s', ], ]; @@ -7996,11 +7998,11 @@ public function dataCallables(): array '$closure()', ], [ - 'Callables\\Bar', + PHP_VERSION_ID < 80000 ? 'Callables\\Bar' : '*ERROR*', '$arrayWithStaticMethod()', ], [ - 'float', + PHP_VERSION_ID < 80000 ? 'float' : '*ERROR*', '$stringWithStaticMethod()', ], [ @@ -8411,6 +8413,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 [ @@ -8540,43 +8588,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 [ @@ -9519,24 +9530,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 @@ -9570,7 +9582,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; } @@ -9581,7 +9593,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; } @@ -9592,7 +9604,7 @@ public function testEarlyTermination(): void }); } - protected function getEarlyTerminatingMethodCalls(): array + protected static function getEarlyTerminatingMethodCalls(): array { return [ \EarlyTermination\Foo::class => [ @@ -9602,7 +9614,7 @@ protected function getEarlyTerminatingMethodCalls(): array ]; } - protected function getEarlyTerminatingFunctionCalls(): array + protected static function getEarlyTerminatingFunctionCalls(): array { return ['baz']; } @@ -9622,7 +9634,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 182b9dea4c..13a3e7f4ca 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'); @@ -80,6 +89,7 @@ public function dataFileAsserts(): iterable 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) { @@ -117,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'); @@ -157,9 +169,12 @@ public function dataFileAsserts(): iterable 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'); @@ -168,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'); @@ -193,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'); @@ -211,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'); @@ -219,6 +252,33 @@ 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'); @@ -255,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'); @@ -279,6 +340,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'); @@ -316,6 +378,11 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/array-sum.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'); @@ -385,6 +452,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'); @@ -558,6 +626,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'); @@ -595,6 +664,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'); @@ -623,12 +696,24 @@ 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) { @@ -679,9 +764,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'); @@ -748,7 +840,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'); @@ -868,6 +962,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'); @@ -875,6 +970,11 @@ 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 { @@ -912,7 +1012,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'); @@ -926,6 +1028,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'); @@ -978,7 +1085,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'); @@ -1024,9 +1133,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'); @@ -1046,6 +1157,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'); @@ -1108,10 +1220,12 @@ 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'); @@ -1131,6 +1245,12 @@ public function dataFileAsserts(): iterable 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'); @@ -1168,9 +1288,11 @@ public function dataFileAsserts(): iterable 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) { @@ -1178,6 +1300,11 @@ public function dataFileAsserts(): iterable } 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) { @@ -1185,19 +1312,163 @@ public function dataFileAsserts(): iterable } 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/trait-instance-of.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/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-10893.php'); } /** diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index e47f09b201..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 { @@ -63,6 +65,7 @@ protected function setUp(): void $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)])); @@ -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( @@ -536,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'), @@ -545,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'))), @@ -572,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', @@ -584,7 +640,7 @@ public function dataCondition(): array '$array' => '~0|0.0|\'\'|\'0\'|array{}|false|null', ], [ - '$array' => 'array{}', + '$array' => 'array{}|null', ], ], [ @@ -718,9 +774,11 @@ public function dataCondition(): array ), [ '$notNullBar' => 'null', + '$barOrNull' => 'null', ], [ '$notNullBar' => '~null', + '$barOrNull' => '~null', ], ], [ @@ -840,9 +898,11 @@ public function dataCondition(): array ), [ '$notNullBar' => '~null', + '$barOrNull' => '~null', ], [ '$notNullBar' => 'null', + '$barOrNull' => 'null', ], ], [ @@ -867,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', ], ], [ @@ -894,9 +977,11 @@ public function dataCondition(): array ), [ '$notFalseBar' => '~false', + '$barOrFalse' => '~false', ], [ '$notFalseBar' => 'false & ' . self::SURE_NOT_TRUTHY, + '$barOrFalse' => 'false', ], ], [ @@ -909,9 +994,11 @@ public function dataCondition(): array ), [ '$notFalseBar' => 'Bar', + '$barOrFalse' => 'Bar', ], [ '$notFalseBar' => '~Bar', + '$barOrFalse' => '~Bar', ], ], [ 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/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/array-chunk-php8.php b/tests/PHPStan/Analyser/data/array-chunk-php8.php new file mode 100644 index 0000000000..056695fe89 --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-chunk-php8.php @@ -0,0 +1,18 @@ + $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 @@ + $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-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-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 @@ '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-push.php b/tests/PHPStan/Analyser/data/array-push.php index 1c726a0c01..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, 17|bool|null>', $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-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 @@ +|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 3c484d8230..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, 17|bool|null>', $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.php b/tests/PHPStan/Analyser/data/array_keys.php index 8adf225a9c..e35f3894d4 100644 --- a/tests/PHPStan/Analyser/data/array_keys.php +++ b/tests/PHPStan/Analyser/data/array_keys.php @@ -15,4 +15,13 @@ public function sayHello($mixed): void 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.php b/tests/PHPStan/Analyser/data/array_values.php index c6d8064b7d..acad5d6abe 100644 --- a/tests/PHPStan/Analyser/data/array_values.php +++ b/tests/PHPStan/Analyser/data/array_values.php @@ -28,4 +28,13 @@ public function foo2($list): void 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-constructor.php b/tests/PHPStan/Analyser/data/assert-constructor.php new file mode 100644 index 0000000000..2d133d8c6c --- /dev/null +++ b/tests/PHPStan/Analyser/data/assert-constructor.php @@ -0,0 +1,22 @@ + + */ +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-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/asymmetric-properties.php b/tests/PHPStan/Analyser/data/asymmetric-properties.php new file mode 100644 index 0000000000..a5a69a5341 --- /dev/null +++ b/tests/PHPStan/Analyser/data/asymmetric-properties.php @@ -0,0 +1,29 @@ +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/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-10147.php b/tests/PHPStan/Analyser/data/bug-10147.php new file mode 100644 index 0000000000..8798de28e7 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10147.php @@ -0,0 +1,13 @@ +", $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-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-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 4b87c1fd27..9dd9838ca8 100644 --- a/tests/PHPStan/Analyser/data/bug-2600-php8.php +++ b/tests/PHPStan/Analyser/data/bug-2600-php8.php @@ -1,6 +1,6 @@ , 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-3142.php b/tests/PHPStan/Analyser/data/bug-3142.php index e46fd7fd22..b1a53ee3cc 100644 --- a/tests/PHPStan/Analyser/data/bug-3142.php +++ b/tests/PHPStan/Analyser/data/bug-3142.php @@ -35,7 +35,7 @@ public function sayHello() function (): void { $hw = new HelloWorld(); assertType('string', $hw->sayHi()); - assertType('int', $hw->sayHello()); + assertType('string', $hw->sayHello()); }; interface DecoratorInterface 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 d7c02c0a0f..468597455b 100644 --- a/tests/PHPStan/Analyser/data/bug-3351.php +++ b/tests/PHPStan/Analyser/data/bug-3351.php @@ -12,7 +12,7 @@ public function sayHello(): void $b = [1, 2, 3]; $c = $this->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-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-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-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 @@ +', $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-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-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-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 329e8e1d8a..c0eef41b30 100644 --- a/tests/PHPStan/Analyser/data/bug-5287-php81.php +++ b/tests/PHPStan/Analyser/data/bug-5287-php81.php @@ -1,6 +1,6 @@ $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 a8fcf21b2b..cf1cda0791 100644 --- a/tests/PHPStan/Analyser/data/bug-5287.php +++ b/tests/PHPStan/Analyser/data/bug-5287.php @@ -34,7 +34,7 @@ function foo3(array $arr): void /** * @param non-empty-array $arr */ -function foo3(array $arr): void +function foo4(array $arr): void { $arrSpread = [...$arr]; assertType('non-empty-list', $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-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-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-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 @@ + assertType(\DateTime::class, $object), - \Throwable::class => assertType('Throwable~DateTime', $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-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-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-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-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-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-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-7115.php b/tests/PHPStan/Analyser/data/bug-7115.php index 9377457745..40263db3fa 100644 --- a/tests/PHPStan/Analyser/data/bug-7115.php +++ b/tests/PHPStan/Analyser/data/bug-7115.php @@ -27,7 +27,7 @@ public function doFoo(): void array_push($d, $thing); } - assertType('array, array{a: int, b: string}>', $b); + assertType('list', $b); assertType('list', $c); assertType('list', $d); } 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-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-7580.php b/tests/PHPStan/Analyser/data/bug-7580.php index 107898e3f8..26fdbc18cb 100644 --- a/tests/PHPStan/Analyser/data/bug-7580.php +++ b/tests/PHPStan/Analyser/data/bug-7580.php @@ -1,6 +1,6 @@ blank($url = $url ?? $this->getUrlForCurrentRequest())) { + return false; + } + + assertType('non-empty-string', $url); + $parsed = parse_url(http://github.com/$url); + + return is_array($parsed); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-7805.php b/tests/PHPStan/Analyser/data/bug-7805.php index 8d59d97184..cf96fe46d5 100644 --- a/tests/PHPStan/Analyser/data/bug-7805.php +++ b/tests/PHPStan/Analyser/data/bug-7805.php @@ -21,10 +21,10 @@ function foo(array $params) assertNativeType("array", $params); $params = $params === [] ? ['list'] : $params; assertType("array{'list'}", $params); - assertNativeType("non-empty-array", $params); + assertNativeType("non-empty-array", $params); array_unshift($params, 'help'); assertType("array{'help', 'list'}", $params); - assertNativeType("non-empty-array", $params); + assertNativeType("non-empty-array", $params); } assertType("array{}|array{'help', 'list'}", $params); assertNativeType('array', $params); diff --git a/tests/PHPStan/Analyser/data/bug-7915.php b/tests/PHPStan/Analyser/data/bug-7915.php new file mode 100644 index 0000000000..1dee1a63d5 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7915.php @@ -0,0 +1,81 @@ + : 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-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-8092.php b/tests/PHPStan/Analyser/data/bug-8092.php new file mode 100644 index 0000000000..52bd9abe1e --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8092.php @@ -0,0 +1,39 @@ + */ +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-8242.php b/tests/PHPStan/Analyser/data/bug-8242.php index 3c14ddc1f8..3e516a7199 100644 --- a/tests/PHPStan/Analyser/data/bug-8242.php +++ b/tests/PHPStan/Analyser/data/bug-8242.php @@ -15,7 +15,7 @@ function f2() { return new NoReturn; } - \PHPStan\Testing\assertType('*NEVER*', f1()); + \PHPStan\Testing\assertType('never', f1()); \PHPStan\Testing\assertType('NoReturn', f2()); } @@ -55,6 +55,6 @@ function f2() { throw new \LogicException(); } - \PHPStan\Testing\assertType('*NEVER*', f1()); - \PHPStan\Testing\assertType('*NEVER*', f2()); + \PHPStan\Testing\assertType('never', f1()); + \PHPStan\Testing\assertType('never', f2()); } diff --git a/tests/PHPStan/Analyser/data/bug-8249.php b/tests/PHPStan/Analyser/data/bug-8249.php new file mode 100644 index 0000000000..960126723d --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8249.php @@ -0,0 +1,36 @@ + 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-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-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-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-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-8664.php b/tests/PHPStan/Analyser/data/bug-8664.php new file mode 100644 index 0000000000..30f8ef38e8 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8664.php @@ -0,0 +1,68 @@ +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-8803.php b/tests/PHPStan/Analyser/data/bug-8803.php new file mode 100644 index 0000000000..a1d9ad568b --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8803.php @@ -0,0 +1,36 @@ +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/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/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/closure-return-type-extensions.php b/tests/PHPStan/Analyser/data/closure-return-type-extensions.php index c171fcffb5..1445bfad9f 100644 --- a/tests/PHPStan/Analyser/data/closure-return-type-extensions.php +++ b/tests/PHPStan/Analyser/data/closure-return-type-extensions.php @@ -18,3 +18,6 @@ $returnType = $closure->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/conditional-types.php b/tests/PHPStan/Analyser/data/conditional-types.php index 98e8550e4c..0cdc741503 100644 --- a/tests/PHPStan/Analyser/data/conditional-types.php +++ b/tests/PHPStan/Analyser/data/conditional-types.php @@ -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.php b/tests/PHPStan/Analyser/data/constant.php new file mode 100644 index 0000000000..c26edcdd53 --- /dev/null +++ b/tests/PHPStan/Analyser/data/constant.php @@ -0,0 +1,40 @@ += 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/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/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-variables-type-guard-same-as-type.php b/tests/PHPStan/Analyser/data/dependent-variables-type-guard-same-as-type.php index f08e83b1b2..db81724a63 100644 --- a/tests/PHPStan/Analyser/data/dependent-variables-type-guard-same-as-type.php +++ b/tests/PHPStan/Analyser/data/dependent-variables-type-guard-same-as-type.php @@ -30,7 +30,7 @@ public function doFoo(): void assertType('int<1, max>', $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-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/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-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/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/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-returns-non-empty-string.php b/tests/PHPStan/Analyser/data/filter-var-returns-non-empty-string.php index 5fa3182493..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 @@ -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); @@ -101,7 +101,7 @@ public function run(string $str, int $int, int $positive_int, int $negative_int) 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 index e5edb1a250..12f97e63b5 100644 --- a/tests/PHPStan/Analyser/data/filter-var.php +++ b/tests/PHPStan/Analyser/data/filter-var.php @@ -8,12 +8,44 @@ class FilterVar { - public function doFoo($mixed): void + /** + * @param array $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('array', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY])); + + 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)); @@ -81,11 +113,11 @@ public function scalars(bool $bool, float $float, int $int, string $string, int 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('bool|null', filter_var(null, 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('float|false', filter_var(true, FILTER_VALIDATE_FLOAT)); // could be 1 - assertType('float|false', filter_var(false, FILTER_VALIDATE_FLOAT)); // could be false + 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)); @@ -96,11 +128,11 @@ public function scalars(bool $bool, float $float, int $int, string $string, int 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('float|false', filter_var(null, FILTER_VALIDATE_FLOAT)); // could be false + assertType('false', filter_var(null, FILTER_VALIDATE_FLOAT)); assertType('int|false', filter_var($bool, FILTER_VALIDATE_INT)); - assertType('int|false', filter_var(true, FILTER_VALIDATE_INT)); // could be 1 - assertType('int|false', filter_var(false, FILTER_VALIDATE_INT)); // could be false + 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)); @@ -111,7 +143,7 @@ public function scalars(bool $bool, float $float, int $int, string $string, 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('int|false', filter_var(null, FILTER_VALIDATE_INT)); // could be false + assertType('false', filter_var(null, FILTER_VALIDATE_INT)); assertType("''|'1'", filter_var($bool)); assertType("'1'", filter_var(true)); 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 @@ (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 3dd5e75807..61d458c703 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())) ); } @@ -1405,7 +1405,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)); }; @@ -1544,8 +1544,8 @@ function (): void { assertType('array{1: true}', arrayBound1([1 => true])); assertType('array{\'a\', \'b\', \'c\'}', arrayBound2(range('a', 'c'))); assertType('array', arrayBound2([1, 2, 3])); - assertType('array{bool, bool, bool}', arrayBound3([true, false, true])); - assertType('array{array{a: string}, array{b: string}, array{c: string}}', arrayBound4([['a' => 'a'], ['b' => 'b'], ['c' => 'c']])); + 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', 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 @@ +', 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,39 @@ 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()); + } + +} 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.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 @@ +returnsAlias()); + } + + /** @psalm-return MyObject */ + public function returnsAlias() + { + + } +} diff --git a/tests/PHPStan/Analyser/data/isset-coalesce-empty-type-root.php b/tests/PHPStan/Analyser/data/isset-coalesce-empty-type-root.php index 29671650c0..2d027da378 100644 --- a/tests/PHPStan/Analyser/data/isset-coalesce-empty-type-root.php +++ b/tests/PHPStan/Analyser/data/isset-coalesce-empty-type-root.php @@ -13,7 +13,7 @@ assertType('bool', isset($sometimesDefinedVariable)); assertType('bool', isset($neverDefinedVariable)); -assertType('mixed', $foo ?? false); +assertType('mixed~null', $foo ?? false); $bar = 'abc'; assertType('\'abc\'', $bar ?? 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 @@ + 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-php83.php b/tests/PHPStan/Analyser/data/mb-strlen-php83.php new file mode 100644 index 0000000000..f46d68a3d0 --- /dev/null +++ b/tests/PHPStan/Analyser/data/mb-strlen-php83.php @@ -0,0 +1,55 @@ +', 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/more-types.php b/tests/PHPStan/Analyser/data/more-types.php new file mode 100644 index 0000000000..9f646300c9 --- /dev/null +++ b/tests/PHPStan/Analyser/data/more-types.php @@ -0,0 +1,44 @@ +|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/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/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 @@ |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-out.php b/tests/PHPStan/Analyser/data/param-out.php index 232eb60ebc..88cd9bf14d 100644 --- a/tests/PHPStan/Analyser/data/param-out.php +++ b/tests/PHPStan/Analyser/data/param-out.php @@ -250,7 +250,8 @@ function fooShuffle() { function fooSort() { $array = ["foo" => 123, "bar" => 456]; sort($array); - assertType('array{foo: 123, bar: 456}', $array); + assertType('non-empty-list<123|456>', $array); + assertType('true', array_is_list($array)); $emptyArray = []; sort($emptyArray); @@ -445,3 +446,58 @@ 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/phpdoc-pseudotype-global.php b/tests/PHPStan/Analyser/data/phpdoc-pseudotype-global.php index b8d6c138de..83c2e56cb6 100644 --- a/tests/PHPStan/Analyser/data/phpdoc-pseudotype-global.php +++ b/tests/PHPStan/Analyser/data/phpdoc-pseudotype-global.php @@ -25,6 +25,6 @@ function () { assertType('float|int|numeric-string', $numeric); assertType('bool', $boolean); assertType('resource', $resource); - assertType('*NEVER*', $never); + assertType('never', $never); assertType('float', $double); }; 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 @@ 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/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/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/self-out.php b/tests/PHPStan/Analyser/data/self-out.php index 39fb2ed6e8..d4de8dbf84 100644 --- a/tests/PHPStan/Analyser/data/self-out.php +++ b/tests/PHPStan/Analyser/data/self-out.php @@ -69,17 +69,17 @@ function () { $i = new a(123); // OK - $i is a<123> assertType('SelfOut\\a', $i); - assertType('void', $i->test()); + assertType('null', $i->test()); $i->addData(321); // OK - $i is a<123|321> assertType('SelfOut\\a', $i); - assertType('void', $i->test()); + assertType('null', $i->test()); $i->setData("test"); // IfThisIsMismatch - Class is not a as required - assertType('SelfOut\\a', $i); - assertType('*NEVER*', $i->test()); + assertType('SelfOut\\a<\'test\'>', $i); + assertType('never', $i->test()); }; function () { @@ -88,4 +88,10 @@ function () { $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/sort.php b/tests/PHPStan/Analyser/data/sort.php new file mode 100644 index 0000000000..e92b006713 --- /dev/null +++ b/tests/PHPStan/Analyser/data/sort.php @@ -0,0 +1,153 @@ + 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/str-shuffle.php b/tests/PHPStan/Analyser/data/str-shuffle.php new file mode 100644 index 0000000000..37aa768525 --- /dev/null +++ b/tests/PHPStan/Analyser/data/str-shuffle.php @@ -0,0 +1,19 @@ +', 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-instance-of.php b/tests/PHPStan/Analyser/data/trait-instance-of.php deleted file mode 100644 index 2d5bdd0b5b..0000000000 --- a/tests/PHPStan/Analyser/data/trait-instance-of.php +++ /dev/null @@ -1,70 +0,0 @@ -', $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/traits-integration.neon b/tests/PHPStan/Analyser/traits-integration.neon new file mode 100644 index 0000000000..dcb115730e --- /dev/null +++ b/tests/PHPStan/Analyser/traits-integration.neon @@ -0,0 +1,2 @@ +parameters: + checkUninitializedProperties: true diff --git a/tests/PHPStan/Analyser/traits/uninitializedProperty/FooClass.php b/tests/PHPStan/Analyser/traits/uninitializedProperty/FooClass.php new file mode 100644 index 0000000000..6995d26c7e --- /dev/null +++ b/tests/PHPStan/Analyser/traits/uninitializedProperty/FooClass.php @@ -0,0 +1,16 @@ +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/Command/AnalysisResultTest.php b/tests/PHPStan/Command/AnalysisResultTest.php index cb34089ee3..2ea3344a9b 100644 --- a/tests/PHPStan/Command/AnalysisResultTest.php +++ b/tests/PHPStan/Command/AnalysisResultTest.php @@ -44,6 +44,8 @@ public function testErrorsAreSortedByFileNameAndLine(): void null, true, 0, + false, + [], ))->getFileSpecificErrors(), ); } diff --git a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php index f07c88f9de..01fd649bf0 100644 --- a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php @@ -149,6 +149,8 @@ public function testFormatErrorMessagesRegexEscape(): void null, true, 0, + false, + [], ); $formatter->formatErrors( $result, @@ -187,6 +189,8 @@ public function testEscapeDiNeon(): void null, true, 0, + false, + [], ); $formatter->formatErrors( @@ -252,6 +256,8 @@ public function testOutputOrdering(array $errors): void null, true, 0, + false, + [], ); $formatter->formatErrors( @@ -410,6 +416,8 @@ public function testEndOfFileNewlines( null, true, 0, + false, + [], ); $resource = fopen('php://memory', 'w', false); diff --git a/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php index 2a16f5d420..9510893f06 100644 --- a/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php @@ -155,6 +155,8 @@ public function testTraitPath(): void null, true, 0, + false, + [], ), $this->getOutput()); $this->assertXmlStringEqualsXmlString(' diff --git a/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php index 73797e890d..c936bf48cc 100644 --- a/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php @@ -253,7 +253,7 @@ 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), $this->getOutput()); + ], [], [], [], [], false, null, true, 0, false, []), $this->getOutput()); $content = $this->getOutputContent(); $json = Json::decode($content, Json::FORCE_ARRAY); diff --git a/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php index fd7ce19b75..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,6 +24,7 @@ protected function setUp(): void protected function tearDown(): void { putenv('COLUMNS'); + putenv('TERM_PROGRAM'); } public function dataFormatterOutputProvider(): iterable @@ -194,6 +196,7 @@ public function testFormatErrors( } $formatter = $this->createErrorFormatter(null); + // NOTE: extra env vars need to be cleared in tearDown() foreach ($extraEnvVars as $envVar) { putenv($envVar); } @@ -210,16 +213,20 @@ 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, 0), $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, 0), $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)); } @@ -228,7 +235,7 @@ 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), $this->getOutput(true)); + $formatter->formatErrors(new AnalysisResult([$error], [], [], [], [], false, null, true, 0, false, []), $this->getOutput(true)); $this->assertStringContainsString('rel/Foo.php:12', $this->getOutputContent(true)); } @@ -254,6 +261,8 @@ public function testBug6727(): void null, true, 0, + false, + [], ), $this->getOutput(), ); diff --git a/tests/PHPStan/Composer/AutoloadFilesTest.php b/tests/PHPStan/Composer/AutoloadFilesTest.php index 0796fc2b64..b16b469e75 100644 --- a/tests/PHPStan/Composer/AutoloadFilesTest.php +++ b/tests/PHPStan/Composer/AutoloadFilesTest.php @@ -61,7 +61,6 @@ public function testExpectedFiles(): void 'jetbrains/phpstorm-stubs/PhpStormStubsMap.php', // added to phpstan-dist/bootstrap.php 'myclabs/deep-copy/src/DeepCopy/deep_copy.php', // dev dependency of PHPUnit 'react/async/src/functions_include.php', // added to phpstan-dist/bootstrap.php - 'react/promise-stream/src/functions_include.php', // added to phpstan-dist/bootstrap.php 'react/promise-timer/src/functions_include.php', // added to phpstan-dist/bootstrap.php 'react/promise/src/functions_include.php', // added to phpstan-dist/bootstrap.php 'ringcentral/psr7/src/functions_include.php', // added to phpstan-dist/bootstrap.php @@ -70,10 +69,10 @@ public function testExpectedFiles(): void 'symfony/polyfill-intl-grapheme/bootstrap.php', // afaik polyfills aren't necessary 'symfony/polyfill-intl-normalizer/bootstrap.php', // afaik polyfills aren't necessary 'symfony/polyfill-mbstring/bootstrap.php', // afaik polyfills aren't necessary - 'symfony/polyfill-php72/bootstrap.php', // afaik polyfills aren't necessary 'symfony/polyfill-php73/bootstrap.php', // afaik polyfills aren't necessary 'symfony/polyfill-php74/bootstrap.php', // afaik polyfills aren't necessary 'symfony/polyfill-php80/bootstrap.php', // afaik polyfills aren't necessary + 'symfony/polyfill-php81/bootstrap.php', // afaik polyfills aren't necessary 'symfony/string/Resources/functions.php', // afaik polyfills aren't necessary ]; 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/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/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..40e142c2fa 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 }, { 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/LevelsIntegrationTest.php b/tests/PHPStan/Levels/LevelsIntegrationTest.php index 75e75dc665..45f5d7634e 100644 --- a/tests/PHPStan/Levels/LevelsIntegrationTest.php +++ b/tests/PHPStan/Levels/LevelsIntegrationTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Levels; use PHPStan\Testing\LevelsTestCase; +use const PHP_VERSION_ID; /** * @group levels @@ -12,7 +13,7 @@ class LevelsIntegrationTest extends LevelsTestCase public function dataTopics(): array { - return [ + $topics = [ ['returnTypes'], ['acceptTypes'], ['methodCalls'], @@ -41,6 +42,11 @@ public function dataTopics(): array ['arrayDestructuring'], ['listType'], ]; + if (PHP_VERSION_ID >= 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/data/acceptTypes-5.json b/tests/PHPStan/Levels/data/acceptTypes-5.json index cc889b6d52..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, diff --git a/tests/PHPStan/Levels/data/acceptTypes-7.json b/tests/PHPStan/Levels/data/acceptTypes-7.json index b43e3e9303..5fab0297d3 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-7.json +++ b/tests/PHPStan/Levels/data/acceptTypes-7.json @@ -54,6 +54,16 @@ "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): Levels\\AcceptTypes\\ParentFooInterface)|(Closure(): Levels\\AcceptTypes\\FooInterface) 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)|(Closure(): Levels\\AcceptTypes\\FooInterface) given.", + "line": 320, + "ignorable": true + }, { "message": "Parameter #1 $i of method Levels\\AcceptTypes\\Baz::doBar() expects int, float|int given.", "line": 415, @@ -129,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, 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/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/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 index 994d485346..9188b4d36d 100644 --- a/tests/PHPStan/Levels/data/stringOffsetAccess-2.json +++ b/tests/PHPStan/Levels/data/stringOffsetAccess-2.json @@ -3,15 +3,5 @@ "message": "PHPDoc tag @var with type int|object is not subtype of native type null.", "line": 7, "ignorable": true - }, - { - "message": "PHPDoc tag @var with type array{baz: 21}|array{foo: 17, bar: 19} is not subtype of native type array{}.", - "line": 54, - "ignorable": true - }, - { - "message": "PHPDoc tag @var with type array{baz: 21}|array{foo: 17, bar: 19} is not subtype of native type array{}.", - "line": 58, - "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 54ad35baa3..d43f7df7d9 100644 --- a/tests/PHPStan/Levels/data/unreachable-4.json +++ b/tests/PHPStan/Levels/data/unreachable-4.json @@ -39,26 +39,51 @@ "line": 74, "ignorable": 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": "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 + }, { "message": "Ternary operator condition is always true.", "line": 89, "ignorable": true }, + { + "message": "Unused result of ternary operator.", + "line": 89, + "ignorable": true + }, { "message": "Ternary operator condition is always true.", "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 +93,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/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/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/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..c42e84cb99 100644 --- a/tests/PHPStan/Parallel/ParallelAnalyserIntegrationTest.php +++ b/tests/PHPStan/Parallel/ParallelAnalyserIntegrationTest.php @@ -80,11 +80,13 @@ public function testRun(string $command): void 'message' => 'Access to an undefined property ParallelAnalyserIntegrationTest\\Foo::$test.', 'line' => 10, 'ignorable' => true, + '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, + '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/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/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/data/only-class.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/only-class.php index 1b66fa0cb0..f38b77dbe9 100644 --- a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/only-class.php +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/only-class.php @@ -1,6 +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 @@ + [ @@ -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/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 bfcbeeca56..00ed1a4159 100644 --- a/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php +++ b/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php @@ -496,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); @@ -535,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/Rules/Api/ApiClassImplementsRuleTest.php b/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php index 4d9c52d716..9b95f57e25 100644 --- a/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php @@ -42,22 +42,22 @@ public function testRuleOutOfPhpStan(): void ], [ 'Implementing PHPStan\Reflection\ReflectionProvider is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 328, + 333, $tip, ], [ 'Implementing PHPStan\Analyser\Scope is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 333, + 338, $tip, ], [ 'Implementing PHPStan\Reflection\FunctionReflection is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 338, + 343, $tip, ], [ 'Implementing PHPStan\Reflection\ExtendedMethodReflection is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 342, + 347, $tip, ], ]); diff --git a/tests/PHPStan/Rules/Api/ApiInstanceofTypeRuleTest.php b/tests/PHPStan/Rules/Api/ApiInstanceofTypeRuleTest.php index e82e085456..6be9f18d4b 100644 --- a/tests/PHPStan/Rules/Api/ApiInstanceofTypeRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiInstanceofTypeRuleTest.php @@ -18,18 +18,27 @@ public function getRule(): Rule 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() instead.', - 19, + '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() instead.', - 23, + '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() instead.', - 35, + '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/data/class-implements-out-of-phpstan.php b/tests/PHPStan/Rules/Api/data/class-implements-out-of-phpstan.php index 20ab02c458..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 @@ -307,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. diff --git a/tests/PHPStan/Rules/Api/data/instanceof-type.php b/tests/PHPStan/Rules/Api/data/instanceof-type.php index 3a096d5b67..eabc5f6c5e 100644 --- a/tests/PHPStan/Rules/Api/data/instanceof-type.php +++ b/tests/PHPStan/Rules/Api/data/instanceof-type.php @@ -2,6 +2,7 @@ namespace ApiInstanceofType; +use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeWithClassName; @@ -35,6 +36,10 @@ public function doFoo($a, Type $type) if ($a instanceof TypeWithClassName) { } + + if ($a instanceof GenericObjectType) { + + } } } diff --git a/tests/PHPStan/Rules/Api/data/static-call-in-phpstan.php b/tests/PHPStan/Rules/Api/data/static-call-in-phpstan.php index c738b63639..f36ed77fa2 100644 --- a/tests/PHPStan/Rules/Api/data/static-call-in-phpstan.php +++ b/tests/PHPStan/Rules/Api/data/static-call-in-phpstan.php @@ -1,6 +1,6 @@ @@ -37,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 b6abc55a97..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, true, false)); + return new IterableInForeachRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false)); } public function testCheckWithMaybes(): void @@ -80,4 +84,60 @@ 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 1447c6c9f6..8db0f9df05 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -15,11 +15,13 @@ class NonexistentOffsetInArrayDimFetchRuleTest extends RuleTestCase private bool $checkExplicitMixed = false; + private bool $checkImplicitMixed = false; + private bool $bleedingEdge = false; protected function getRule(): Rule { - $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, false, true, false); + $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false); return new NonexistentOffsetInArrayDimFetchRule( $ruleLevelHelper, @@ -105,18 +107,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, @@ -250,18 +244,10 @@ public function testRuleBleedingEdge(): 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, @@ -552,14 +538,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, - ], ]); } @@ -711,4 +689,68 @@ public function testBug8356(): void ]); } + 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, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php b/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php index 2c83a3e119..0129923ae8 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php @@ -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 8306d8040d..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, true, 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/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-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-8166.php b/tests/PHPStan/Rules/Arrays/data/bug-8166.php new file mode 100644 index 0000000000..db68b7cf6a --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-8166.php @@ -0,0 +1,24 @@ + $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-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 b91ce4ba02..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 @@ -39,3 +39,10 @@ /** @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/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/InvalidCastRuleTest.php b/tests/PHPStan/Rules/Cast/InvalidCastRuleTest.php index 89100fa22c..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, true, false)); + return new InvalidCastRule($broker, new RuleLevelHelper($broker, true, false, true, $this->checkExplicitMixed, $this->checkImplicitMixed, true, false)); } public function testRule(): void @@ -82,4 +88,84 @@ public function testCastObjectToString(): void ]); } + 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/data/mixed-cast.php b/tests/PHPStan/Rules/Cast/data/mixed-cast.php new file mode 100644 index 0000000000..73085a755e --- /dev/null +++ b/tests/PHPStan/Rules/Cast/data/mixed-cast.php @@ -0,0 +1,31 @@ += 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/ClassAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php index 42201f8276..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; @@ -38,7 +40,10 @@ 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 cd0391d560..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; @@ -37,7 +39,10 @@ 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 0cdf5886b1..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, true, 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 @@ -373,6 +383,40 @@ public function testClassConstFetchDefined(): void '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/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 a3877f2c5f..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, ], [ @@ -70,14 +75,4 @@ public function testBug7720(): void ]); } - public function testTraitInstanceOf(): void - { - $this->analyse([__DIR__ . '/../../Analyser/data/trait-instance-of.php'], [ - [ - 'Instanceof between $this(TraitInstanceOf\ATrait1Class) and trait TraitInstanceOf\Trait2 will always evaluate to false.', - 21, - ], - ]); - } - } 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 9ca074d5b1..271ab2dd83 100644 --- a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php @@ -16,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 @@ -378,10 +380,12 @@ public function testBug8042(): void [ '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.', ], ]); } @@ -417,6 +421,7 @@ public function testUnreachableIfBranches(): void [ 'Instanceof between stdClass and stdClass will always evaluate to true.', 37, + 'Remove remaining cases below this one and this error will disappear too.', ], ]); } @@ -433,10 +438,12 @@ public function testIfBranchesDoNotReportPhpDoc(): void [ '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.', ], ]); } @@ -454,10 +461,12 @@ public function testIfBranchesReportPhpDoc(): void [ '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.', @@ -467,10 +476,12 @@ public function testIfBranchesReportPhpDoc(): void [ '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.', ], ]); } @@ -504,6 +515,10 @@ public function testTernaryElseDoNotReportPhpDoc(): void '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, + ], ]); } @@ -529,7 +544,6 @@ public function testTernaryElseReportPhpDoc(): void [ 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', 20, - $tipText, ], ]); } @@ -541,11 +555,72 @@ public function testBug4689(): void $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 = false; - $this->analyse([__DIR__ . '/data/bug-3632.php'], []); + $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 b91fd7fa2d..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, true, 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, + ], ]); } @@ -446,4 +455,42 @@ public function testBug3311a(): void ]); } + 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 9b576db978..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,7 +24,10 @@ 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(true, true, true, true, []), new UnresolvableTypeHelper(), @@ -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/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-3632.php b/tests/PHPStan/Rules/Classes/data/bug-3632.php index be2f9186ed..d21950cd6c 100644 --- a/tests/PHPStan/Rules/Classes/data/bug-3632.php +++ b/tests/PHPStan/Rules/Classes/data/bug-3632.php @@ -1,33 +1,41 @@ -test(); + if(rand(2,10) == 4) { + $a = new NiceClass(); } -} -class OtherClass { - use Foo; + if(rand(2,10) == 8) { + $b = new NiceClass(); + } - function bar(): string { - return $this->test(); + if (!$a instanceof NiceClass && !$b instanceof NiceClass) { + // none have been set, ignore + return null; } -} + + if ($a instanceof NiceClass && $b instanceof NiceClass) { + // both have been set, ignore + return null; + } + + if ($a instanceof NiceClass && !$b instanceof NiceClass) { + return 'A has been added'; + } + + if (!$a instanceof NiceClass && $b instanceof NiceClass) { + return 'A has been removed'; + } + + return '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 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 index 529a7f2ca0..618fc491b2 100644 --- a/tests/PHPStan/Rules/Classes/data/class-const-fetch-defined.php +++ b/tests/PHPStan/Rules/Classes/data/class-const-fetch-defined.php @@ -47,5 +47,15 @@ public static function doFoo() \Foo::TEST; \ClassConstFetchDefined\Foo::TEST; } + + if (defined('Foo::TEST') === true) { + Foo::TEST; + \Foo::TEST; + \ClassConstFetchDefined\Foo::TEST; + } else { + Foo::TEST; + \Foo::TEST; + \ClassConstFetchDefined\Foo::TEST; + } } } diff --git a/tests/PHPStan/Rules/Classes/data/enum-sanity.php b/tests/PHPStan/Rules/Classes/data/enum-sanity.php index d8f84b9111..1698b595fd 100644 --- a/tests/PHPStan/Rules/Classes/data/enum-sanity.php +++ b/tests/PHPStan/Rules/Classes/data/enum-sanity.php @@ -83,6 +83,37 @@ public function __unserialize(array $data) { } } +enum EnumDuplicateValue: int { + case A = 1; + case B = 2; + case C = 2; + case D = 3; + case E = 1; +} + +enum ValidIntBackedEnum: int { + case A = 1; + case B = 2; +} + +enum ValidStringBackedEnum: string { + case A = 'A'; + case B = 'B'; +} + +enum EnumInconsistentCaseType: int { + case FOO = 'foo'; + case BAR; +} + +enum EnumInconsistentStringCaseType: string { + case BAR; +} + +enum EnumWithValueButNotBacked { + case FOO = 1; +} + enum EnumMayNotSerializable implements \Serializable { public function serialize() { diff --git a/tests/PHPStan/Rules/Classes/data/extends-readonly-class.php b/tests/PHPStan/Rules/Classes/data/extends-readonly-class.php new file mode 100644 index 0000000000..cf134c3a2b --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/extends-readonly-class.php @@ -0,0 +1,41 @@ + '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 35f43c01ab..6ebe5bc544 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php @@ -15,6 +15,8 @@ class BooleanAndConstantConditionRuleTest extends RuleTestCase private bool $bleedingEdge = false; + private bool $reportAlwaysTrueInLastCondition = false; + protected function getRule(): Rule { return new BooleanAndConstantConditionRule( @@ -31,6 +33,7 @@ protected function getRule(): Rule ), $this->treatPhpDocTypesAsCertain, $this->bleedingEdge, + $this->reportAlwaysTrueInLastCondition, ); } @@ -122,10 +125,12 @@ public function testRule(): void [ '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.', ], ]); } @@ -427,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 4324c57b9b..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( @@ -29,6 +31,7 @@ protected function getRule(): Rule true, ), $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, ); } @@ -68,6 +71,7 @@ public function testRule(): void [ 'Negated boolean expression is always true.', 67, + 'Remove remaining cases below this one and this error will disappear too.', ], ]); } @@ -148,4 +152,52 @@ public function testBug8797(): void $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 8b58ee2364..b8b0777ca2 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php @@ -16,6 +16,8 @@ class BooleanOrConstantConditionRuleTest extends RuleTestCase private bool $bleedingEdge = false; + private bool $reportAlwaysTrueInLastCondition = false; + protected function getRule(): Rule { return new BooleanOrConstantConditionRule( @@ -32,6 +34,7 @@ protected function getRule(): Rule ), $this->treatPhpDocTypesAsCertain, $this->bleedingEdge, + $this->reportAlwaysTrueInLastCondition, ); } @@ -114,10 +117,12 @@ public function testRule(): void [ '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.', ], ]); } @@ -366,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 d229aef87b..f8a0b7d87d 100644 --- a/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php @@ -14,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 @@ -35,6 +39,11 @@ public function testRule(): void "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%.', + ], ]); } @@ -61,6 +70,12 @@ public function testRuleAlwaysTrue(): void [ "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%.', ], ]); } @@ -96,4 +111,59 @@ public function testBug8485(): void ]); } + 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/ElseIfConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php index d0deedb8b7..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( @@ -28,6 +30,7 @@ protected function getRule(): Rule true, ), $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, ); } @@ -36,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 @@ -54,6 +99,7 @@ public function testDoNotReportPhpDoc(): void [ 'Elseif condition is always true.', 46, + 'Remove remaining cases below this one and this error will disappear too.', ], ]); } @@ -65,11 +111,12 @@ public function testReportPhpDoc(): void [ 'Elseif condition is always true.', 46, + 'Remove remaining cases below this one and this error will disappear too.', ], [ 'Elseif condition is always true.', 56, - 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + '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 fb1d650c25..979af0dc12 100644 --- a/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php @@ -144,4 +144,22 @@ public function testBug8485(): void $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 897eac2ee5..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( @@ -29,6 +35,7 @@ protected function getRule(): Rule ), $this->checkAlwaysTrueCheckTypeFunctionCall, $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, ); } @@ -247,6 +254,12 @@ public function testImpossibleCheckTypeFunctionCall(): void [ '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%.', ], ], ); @@ -353,6 +366,11 @@ public function testImpossibleCheckTypeFunctionCallWithoutAlwaysTrue(): void '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%.', + ], ], ); } @@ -383,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%.', + ], ]); } @@ -509,6 +537,11 @@ public function testBugInArrayDateFormat(): void '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%.', + ], ]); } @@ -706,6 +739,13 @@ public function testBug8474(): void $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; @@ -713,6 +753,13 @@ public function testBug8752(): void $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; @@ -749,4 +796,263 @@ public function testImpossibleMethodExistOnGenericClassString(): void ]); } + 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 37ae369e9c..9be90b8054 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php @@ -23,6 +23,7 @@ public function getRule(): Rule ), true, true, + false, ); } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php index 9a549b20eb..3aa4bf0810 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php @@ -23,6 +23,7 @@ public function getRule(): Rule ), true, true, + false, ); } @@ -122,6 +123,7 @@ public function testRule(): void [ '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 eecf6c179b..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( @@ -25,6 +27,7 @@ public function getRule(): Rule ), true, $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, ); } @@ -154,6 +157,7 @@ public function testRule(): void [ '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.', ], ]); } @@ -212,6 +216,38 @@ public function testBug8169(): void ]); } + 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 dec269ea12..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( @@ -25,6 +27,7 @@ public function getRule(): Rule ), true, $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, ); } @@ -64,6 +67,7 @@ public function testRule(): void [ '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.', ], ]); } @@ -109,6 +113,38 @@ public function testAssertUnresolvedGeneric(): void $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 bd4d806f7c..b267bfc3e1 100644 --- a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php @@ -35,6 +35,7 @@ protected function getRule(): Rule true, $this->disableUnreachable, $this->reportAlwaysTrueInLastCondition, + $this->treatPhpDocTypesAsCertain, ); } @@ -358,6 +359,15 @@ public function dataReportAlwaysTrueInLastCondition(): iterable '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, [ [ @@ -372,6 +382,18 @@ public function dataReportAlwaysTrueInLastCondition(): iterable '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, [ [ @@ -379,6 +401,11 @@ public function dataReportAlwaysTrueInLastCondition(): iterable 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, [ [ @@ -389,6 +416,14 @@ public function dataReportAlwaysTrueInLastCondition(): iterable '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, + ], ]]; } @@ -407,4 +442,105 @@ public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLast $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 1716098882..78cef84972 100644 --- a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php @@ -207,4 +207,28 @@ public function testBug7075(): void $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 d0a8d77f9f..d8397ed533 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -32,6 +32,7 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool 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'], [ @@ -58,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.', @@ -110,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.', @@ -182,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.', @@ -267,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'], [ @@ -285,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.', @@ -377,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.', @@ -572,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%.', ], ]); } @@ -607,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 @@ -628,6 +645,7 @@ public function testBug8485(): void [ '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.', @@ -648,12 +666,12 @@ public function testBug8485(): void [ '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.', + "• 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.', + "• Remove remaining cases below this one and this error will disappear too.\n• Use match expression instead. PHPStan will report unhandled enum cases.", ], ]); } @@ -693,14 +711,17 @@ public function testBug4242(): void 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.', @@ -709,38 +730,47 @@ public function testBug3633(): void [ '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.', @@ -813,6 +843,11 @@ public function dataLastMatchArm(): iterable 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, [ [ @@ -839,6 +874,14 @@ public function dataLastMatchArm(): iterable "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, + ], ]]; } @@ -869,4 +912,108 @@ public function testBug8776Part2(): void $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/UnreachableIfBranchesRuleTest.php b/tests/PHPStan/Rules/Comparison/UnreachableIfBranchesRuleTest.php index 28ecef4dd1..7843308281 100644 --- a/tests/PHPStan/Rules/Comparison/UnreachableIfBranchesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/UnreachableIfBranchesRuleTest.php @@ -130,4 +130,10 @@ public function testBug8562(): void $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 b2acd265a8..317f5f98aa 100644 --- a/tests/PHPStan/Rules/Comparison/UnreachableTernaryElseBranchRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/UnreachableTernaryElseBranchRuleTest.php @@ -64,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, + ], ]); } @@ -88,7 +92,6 @@ public function testReportPhpDoc(): void [ 'Else branch is unreachable because ternary operator condition is always true.', 20, - $tipText, ], ]); } @@ -99,4 +102,10 @@ public function testBug3019(): void $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/data/boolean-and-report-always-true-last-condition.php b/tests/PHPStan/Rules/Comparison/data/boolean-and-report-always-true-last-condition.php new file mode 100644 index 0000000000..5e69abc3af --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/boolean-and-report-always-true-last-condition.php @@ -0,0 +1,88 @@ + $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 @@ += 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-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-7491.php b/tests/PHPStan/Rules/Comparison/data/bug-7491.php new file mode 100644 index 0000000000..8990e0f0ff --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-7491.php @@ -0,0 +1,14 @@ + $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-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-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-8900.php b/tests/PHPStan/Rules/Comparison/data/bug-8900.php new file mode 100644 index 0000000000..1dd0e78839 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8900.php @@ -0,0 +1,25 @@ += 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 8e093b90eb..d9fb0b0845 100644 --- a/tests/PHPStan/Rules/Comparison/data/check-type-function-call.php +++ b/tests/PHPStan/Rules/Comparison/data/check-type-function-call.php @@ -891,3 +891,79 @@ public function sayHello(?int $date): void } } } + +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/elseif-condition.php b/tests/PHPStan/Rules/Comparison/data/elseif-condition.php index ee9bc23a9a..822401e846 100644 --- a/tests/PHPStan/Rules/Comparison/data/elseif-condition.php +++ b/tests/PHPStan/Rules/Comparison/data/elseif-condition.php @@ -1,6 +1,6 @@ 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-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 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 @@ + 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 e70f3a7fe3..37d5e01c1e 100644 --- a/tests/PHPStan/Rules/Comparison/data/match-expr.php +++ b/tests/PHPStan/Rules/Comparison/data/match-expr.php @@ -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/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-match-arm.php b/tests/PHPStan/Rules/Comparison/data/strict-comparison-last-match-arm.php index 195ece99ea..d601468e1c 100644 --- a/tests/PHPStan/Rules/Comparison/data/strict-comparison-last-match-arm.php +++ b/tests/PHPStan/Rules/Comparison/data/strict-comparison-last-match-arm.php @@ -64,4 +64,21 @@ public function doIpsum(): void }; } + 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 90aae5b3a3..9719e9c133 100644 --- a/tests/PHPStan/Rules/Comparison/data/strict-comparison.php +++ b/tests/PHPStan/Rules/Comparison/data/strict-comparison.php @@ -983,7 +983,7 @@ function () { NAN !== NAN; }; -class ArrayWithLongStrings +class ArrayWithLongStrings2 { public function doFoo() 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 @@ + */ +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/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 e1d3e2382f..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 @@ -37,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/dynamic-class-constant-fetch.php b/tests/PHPStan/Rules/Constants/data/dynamic-class-constant-fetch.php new file mode 100644 index 0000000000..66ea350f8d --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/dynamic-class-constant-fetch.php @@ -0,0 +1,21 @@ += 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/DeadCode/NoopRuleTest.php b/tests/PHPStan/Rules/DeadCode/NoopRuleTest.php index d9f5358b8f..48864332f9 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()), true); } public function testRule(): void @@ -77,6 +77,37 @@ public function testRule(): void '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, + ], ]); } diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php index e0b4d4ce6c..0efdb81138 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -41,11 +41,19 @@ public function testRule(): void ], [ 'Unreachable statement - code above always terminates.', - 71, + 44, ], [ 'Unreachable statement - code above always terminates.', - 135, + 58, + ], + [ + 'Unreachable statement - code above always terminates.', + 93, + ], + [ + 'Unreachable statement - code above always terminates.', + 157, ], ]); } @@ -141,4 +149,73 @@ public function testBug8620(): void $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 5fe483b857..e17c06a69d 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php @@ -272,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-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-8850.php b/tests/PHPStan/Rules/DeadCode/data/bug-8850.php new file mode 100644 index 0000000000..96ab4f318a --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-8850.php @@ -0,0 +1,50 @@ +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/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 b43f2507fb..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 */ 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..77f45bfef1 100644 --- a/tests/PHPStan/Rules/Debug/DumpTypeRuleTest.php +++ b/tests/PHPStan/Rules/Debug/DumpTypeRuleTest.php @@ -72,4 +72,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/EnumCases/EnumCaseAttributesRuleTest.php b/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php index 9edc0ef941..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; @@ -37,7 +39,10 @@ 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/CatchWithUnthrownExceptionRuleTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php index 2d403c70ac..e209473368 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,132 @@ 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, + ], + ]); + } + + 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 +306,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'], [ @@ -435,4 +584,32 @@ public function testMagicMethods(): void ]); } + 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/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/TooWideMethodThrowTypeRuleTest.php b/tests/PHPStan/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php index eddbd31bf6..d40a19b5f1 100644 --- a/tests/PHPStan/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/TooWideMethodThrowTypeRuleTest.php @@ -43,4 +43,9 @@ public function testRule(): void ]); } + public function testBug6233(): void + { + $this->analyse([__DIR__ . '/data/bug-6233.php'], []); + } + } 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/missing-exception-method-throws.php b/tests/PHPStan/Rules/Exceptions/data/missing-exception-method-throws.php index 51cd4b0ef0..7df5ea9915 100644 --- a/tests/PHPStan/Rules/Exceptions/data/missing-exception-method-throws.php +++ b/tests/PHPStan/Rules/Exceptions/data/missing-exception-method-throws.php @@ -95,4 +95,9 @@ 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/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..9254768fc8 100644 --- a/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php +++ b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php @@ -544,3 +544,35 @@ 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) { + + } + } + +} 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 1c62d47af2..95ac9ed295 100644 --- a/tests/PHPStan/Rules/Functions/ArrayFilterRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrayFilterRuleTest.php @@ -20,6 +20,7 @@ protected function getRule(): Rule 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.', @@ -40,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.', @@ -60,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.', @@ -72,10 +75,13 @@ public function testFile(): void 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, ], ]; 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 dfad3ae5b2..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; @@ -37,7 +39,10 @@ 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 5e3223a334..bf46cd56a6 100644 --- a/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php @@ -39,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, + ], ]); } @@ -56,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 f5f2fcf17d..bcd1c9ee87 100644 --- a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php @@ -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, ], ], @@ -267,4 +276,44 @@ 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 b2845e8e1b..fa27f3acc5 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -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, ], [ @@ -862,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); @@ -903,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, ], ]); @@ -1123,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'], []); @@ -1188,39 +1205,43 @@ 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.', - 26, + 28, + ], + [ + 'Parameter #3 $value of function curl_setopt expects array, array given.', + 67, ], ]); } @@ -1235,4 +1256,399 @@ 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 testBug10297(): void + { + $this->analyse([__DIR__ . '/data/bug-10297.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php b/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php index 0e5af980b7..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) { @@ -191,4 +244,18 @@ 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 8f5fcb4e6b..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; @@ -37,7 +39,10 @@ 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 5fe0446016..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 @@ -128,4 +129,13 @@ 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/ExistingClassesInArrowFunctionTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php index d699e8efaf..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 @@ -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, + ], ], ], ]; @@ -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 f3e02354dd..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 @@ -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, + ], ], ], ]; diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php index 31de1353ad..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 @@ -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, + ], ], ], ]; diff --git a/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php index 0491852e08..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; @@ -37,7 +39,10 @@ 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/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/ParamAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php index ee517d6e74..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; @@ -37,7 +39,10 @@ protected function getRule(): Rule true, true, ), - new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, false), + new ClassForbiddenNameCheck(self::getContainer()), + ), true, ), ); @@ -66,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 20dadf918c..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 @@ -185,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-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-5986.php b/tests/PHPStan/Rules/Functions/data/bug-5986.php new file mode 100644 index 0000000000..48cc9fe103 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5986.php @@ -0,0 +1,35 @@ + */ + 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-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 @@ + + */ +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 @@ + $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-spaceship.php b/tests/PHPStan/Rules/Functions/data/bug-spaceship.php new file mode 100644 index 0000000000..3200b58dc7 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-spaceship.php @@ -0,0 +1,6 @@ + $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 @@ +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 @@ + (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/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/YieldTypeRuleTest.php b/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php index 9a8214c1ad..deec20a074 100644 --- a/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php @@ -66,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 d0d53a3a9f..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, [], ), @@ -108,6 +108,28 @@ public function testRuleExtends(): void 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, + ], ]); } @@ -194,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, + ], ]); } @@ -232,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 07d3c237b8..8f743f4dc7 100644 --- a/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php +++ b/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php @@ -173,6 +173,64 @@ public function testRule(): void '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..07cbd0b860 100644 --- a/tests/PHPStan/Rules/Generics/data/bug-3769.php +++ b/tests/PHPStan/Rules/Generics/data/bug-3769.php @@ -70,8 +70,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 b8f99af6a5..bb942838ed 100644 --- a/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php +++ b/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php @@ -221,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 index 2b5394497a..311958192a 100644 --- a/tests/PHPStan/Rules/Generics/data/method-signature-variance-contravariant.php +++ b/tests/PHPStan/Rules/Generics/data/method-signature-variance-contravariant.php @@ -72,4 +72,12 @@ 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-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-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/Keywords/DeclareStrictTypesRuleTest.php b/tests/PHPStan/Rules/Keywords/DeclareStrictTypesRuleTest.php new file mode 100644 index 0000000000..7aac5f8019 --- /dev/null +++ b/tests/PHPStan/Rules/Keywords/DeclareStrictTypesRuleTest.php @@ -0,0 +1,107 @@ + + */ +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 +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, + ], + ]); + } + } 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/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index fb770c4c14..2219328e27 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -40,6 +40,35 @@ protected function getRule(): Rule ); } + 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,7 +500,11 @@ 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, ], [ @@ -791,7 +824,11 @@ 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, ], [ @@ -931,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, + ], ]); } @@ -1518,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, @@ -1529,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.', @@ -1570,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, @@ -1741,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, + ], ]); } @@ -2151,6 +2201,11 @@ 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, @@ -2161,6 +2216,11 @@ public function testBug5372(): void 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, @@ -2200,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, ], [ @@ -2596,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, + ], ]); } @@ -2772,28 +2836,32 @@ public function dataCallablesWithoutCheckNullables(): iterable $errors = [ [ - 'Parameter #1 $cb of method CallablesWithoutCheckNullables\Foo::doBar() expects callable(float|null): float|null, Closure(float): float given.', + '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.', + '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.', + '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.', + '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]; @@ -2812,6 +2880,19 @@ public function testCallablesWithoutCheckNullables(bool $checkNullables, bool $c $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; @@ -2847,4 +2928,331 @@ public function testCannotCallOnGenericClassString(): void ]); } + 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. + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index a5b31d3f22..f9886774fc 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, true, 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,8 +450,35 @@ 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'], [ + [ + 'Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array{class-string, \'sayHello2\'} given.', + 16, + ], + ]); + } + + 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, @@ -562,4 +616,216 @@ public function testBug6147(): void $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 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/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 10e31e1ab7..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 @@ -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, + ], ], ], ]; @@ -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, ], ], 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 325115301c..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; @@ -39,7 +41,10 @@ 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/MethodSignatureRuleTest.php b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php index c2e1a1682c..7c1eb8446c 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, true, - new MethodParameterComparisonHelper($phpVersion), + false, ); } @@ -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.', ], @@ -414,4 +421,105 @@ public function testListReturnTypeCovariance(): void ]); } + 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/MissingMethodReturnTypehintRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php index 9c415a83f2..a4e64d8b6f 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php @@ -83,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 4ca3e19dc7..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, ], ]; @@ -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.', ], @@ -520,6 +533,9 @@ 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 49deeda23f..15c54115c8 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -414,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.', ], ]); } @@ -779,6 +780,7 @@ public function testBug8071(): void // 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.', ], ]); } @@ -839,4 +841,166 @@ 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 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 0f5b3087ed..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; @@ -23,7 +25,16 @@ protected function getRule(): Rule $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), ); } diff --git a/tests/PHPStan/Rules/Methods/VirtualNullsafeMethodCallTest.php b/tests/PHPStan/Rules/Methods/VirtualNullsafeMethodCallTest.php new file mode 100644 index 0000000000..ae4499167e --- /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('')->build()]; + } + + return [RuleErrorBuilder::message('Regular method call detected')->identifier('')->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-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-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 @@ +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-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-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 @@ + + */ + 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-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 @@ += 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/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 @@ +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 @@ + */ + 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 @@ +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/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-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/tentative-return-types.php b/tests/PHPStan/Rules/Methods/data/tentative-return-types.php index 5b6cecf954..0471f4a578 100644 --- a/tests/PHPStan/Rules/Methods/data/tentative-return-types.php +++ b/tests/PHPStan/Rules/Methods/data/tentative-return-types.php @@ -93,3 +93,16 @@ public function rewind() } } + + +abstract class MetadataFilter extends \FilterIterator +{ + /** + * @return \ArrayIterator + */ + #[\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 27deea557f..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; @@ -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 b5d75392e5..7729265a85 100644 --- a/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php @@ -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/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 b58474f9a7..e2b1bdd40f 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,24 @@ 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, + ], ]); } @@ -268,4 +305,133 @@ public function testParamOut(): void ]); } + 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 19a9185834..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, + ], ]); } @@ -141,4 +169,30 @@ 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/InvalidPHPStanDocTagRuleTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php index 92188b06f4..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,30 +14,59 @@ 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..f8158edb1e --- /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', + 12, + ], + ]); + } + + 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 c7e1a161e7..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,11 +20,14 @@ 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(true, true, true, true, []), new UnresolvableTypeHelper(), @@ -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 index 3e2d87ea5a..932a09c299 100644 --- a/tests/PHPStan/Rules/PhpDoc/MethodAssertRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/MethodAssertRuleTest.php @@ -57,4 +57,19 @@ public function testRule(): void ]); } + 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 87eaf863a2..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,6 +63,10 @@ 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, + ], ]); } @@ -80,4 +80,19 @@ public function testBug8284(): void ]); } + 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 index 40155e9335..f3ea56d12f 100644 --- a/tests/PHPStan/Rules/PhpDoc/VarTagChangedExpressionTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/VarTagChangedExpressionTypeRuleTest.php @@ -13,7 +13,7 @@ class VarTagChangedExpressionTypeRuleTest extends RuleTestCase protected function getRule(): Rule { - return new VarTagChangedExpressionTypeRule(new VarTagTypeRuleHelper(true)); + return new VarTagChangedExpressionTypeRule(new VarTagTypeRuleHelper(true, true)); } public function testRule(): void @@ -48,4 +48,26 @@ public function testAssignOfDifferentVariable(): void ]); } + 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 7fefce14d6..bde4d432ae 100644 --- a/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php @@ -17,11 +17,13 @@ class WrongVariableNameInVarTagRuleTest extends RuleTestCase private bool $checkTypeAgainstPhpDocType = false; + private bool $strictWideningCheck = false; + protected function getRule(): Rule { return new WrongVariableNameInVarTagRule( self::getContainer()->getByType(FileTypeMapper::class), - new VarTagTypeRuleHelper($this->checkTypeAgainstPhpDocType), + new VarTagTypeRuleHelper($this->checkTypeAgainstPhpDocType, $this->strictWideningCheck), $this->checkTypeAgainstNativeType, ); } @@ -196,8 +198,7 @@ public function testEnums(): void public function dataReportWrongType(): iterable { - yield [false, false, []]; - yield [true, false, [ + $nativeCheckOnly = [ [ 'PHPDoc tag @var with type string|null is not subtype of native type string.', 14, @@ -226,9 +227,129 @@ public function dataReportWrongType(): iterable '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 [false, true, []]; - yield [true, true, [ + yield [true, true, true, [ [ 'PHPDoc tag @var with type string|null is not subtype of native type string.', 14, @@ -245,6 +366,10 @@ public function dataReportWrongType(): iterable '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, @@ -257,6 +382,10 @@ public function dataReportWrongType(): iterable '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.', @@ -270,6 +399,10 @@ public function dataReportWrongType(): iterable '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, @@ -286,6 +419,14 @@ public function dataReportWrongType(): iterable '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, @@ -294,17 +435,79 @@ public function dataReportWrongType(): iterable '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, array $expectedErrors): void + 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-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-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-9055.php b/tests/PHPStan/Rules/PhpDoc/data/bug-9055.php new file mode 100644 index 0000000000..a700c24a1e --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-9055.php @@ -0,0 +1,20 @@ += 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-types.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php index 5f7fd45b73..9d5deafd01 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php @@ -284,3 +284,39 @@ 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) +{ + +} 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..04dd9ca0ba --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-type-type-alias.php @@ -0,0 +1,17 @@ + $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-conditional-return-type.php b/tests/PHPStan/Rules/PhpDoc/data/method-conditional-return-type.php index c8f0cb05fd..7829e8c71a 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/method-conditional-return-type.php +++ b/tests/PHPStan/Rules/PhpDoc/data/method-conditional-return-type.php @@ -175,3 +175,15 @@ 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/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/wrong-var-native-type.php b/tests/PHPStan/Rules/PhpDoc/data/wrong-var-native-type.php index 55dcd5b026..2edb6ba496 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/wrong-var-native-type.php +++ b/tests/PHPStan/Rules/PhpDoc/data/wrong-var-native-type.php @@ -170,3 +170,69 @@ 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..323ddd7b36 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; 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/data/function-never.php b/tests/PHPStan/Rules/Playground/data/function-never.php new file mode 100644 index 0000000000..68087469ae --- /dev/null +++ b/tests/PHPStan/Rules/Playground/data/function-never.php @@ -0,0 +1,50 @@ + */ +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/Properties/AccessPropertiesInAssignRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php index 038b001cf8..9b70004995 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php @@ -23,14 +23,17 @@ protected function getRule(): Rule 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 ea1ebd0b74..4673409880 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php @@ -31,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.', @@ -57,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.', @@ -78,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.', @@ -112,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.', @@ -128,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.', @@ -148,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.', @@ -162,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.', @@ -188,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.', @@ -209,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.', @@ -243,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.', @@ -251,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.', @@ -263,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, ], ], ); @@ -276,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, ], ]); } @@ -289,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.', @@ -309,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.', @@ -321,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.', @@ -337,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, ], ]); } @@ -350,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, ], ]); } @@ -393,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, ], ]); } @@ -415,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.', @@ -436,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, + ], ]); } @@ -500,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, ], ]); } @@ -564,10 +627,12 @@ 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, ], ]; @@ -575,26 +640,32 @@ public function dataDynamicProperties(): array [ '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); @@ -602,14 +673,17 @@ public function dataDynamicProperties(): array [ 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', 26, + $tipText, ], [ 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', 27, + $tipText, ], [ 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', 28, + $tipText, ], ]); @@ -617,26 +691,32 @@ public function dataDynamicProperties(): array [ '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, ], ]; @@ -645,6 +725,7 @@ public function dataDynamicProperties(): array [ 'Access to an undefined property DynamicProperties\Baz::$dynamicProperty.', 23, + $tipText, ], ] : array_merge($errors, $otherErrors)], [true, array_merge($errorsWithMore, $otherErrors)], @@ -702,29 +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\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; @@ -739,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; @@ -783,4 +872,69 @@ public function testBug393(): void $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 174d06ad24..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, true, 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 3b71c235fb..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; @@ -20,7 +22,10 @@ protected function getRule(): Rule return new AccessStaticPropertiesRule( $reflectionProvider, new RuleLevelHelper($reflectionProvider, true, false, true, false, false, true, false), - new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassNameCheck( + new ClassCaseSensitivityCheck($reflectionProvider, true), + new ClassForbiddenNameCheck(self::getContainer()), + ), ); } diff --git a/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php index ee02ea4ac6..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, 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 bd01d218e9..747be201bb 100644 --- a/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php @@ -51,6 +51,11 @@ public function testRule(): void 'Property MissingPropertyTypehint\CallableSignature::$cb type has no signature specified for callable.', 93, ], + [ + 'Property MissingPropertyTypehint\NestedArrayInProperty::$args type has no value type specified in iterable type array.', + 103, + 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/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 f8e50f870e..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; @@ -36,7 +38,10 @@ 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/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php index bac6bace61..197473fbf5 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', ], 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/ReadingWriteOnlyPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/ReadingWriteOnlyPropertiesRuleTest.php index 51cc7ccaee..d5834f4003 100644 --- a/tests/PHPStan/Rules/Properties/ReadingWriteOnlyPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadingWriteOnlyPropertiesRuleTest.php @@ -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 7622a3c320..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, true, 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 7136107a79..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, true, 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 @@ -515,7 +515,7 @@ public function testBug3311b(): void [ 'Property Bug3311b\Foo::$bar (list) does not accept non-empty-array, string>.', 16, - 'array, string> might not be a list.', + 'non-empty-array, string> might not be a list.', ], ]); } @@ -526,4 +526,86 @@ public function testBug7789(): void $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..477eafc4d5 --- /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('')->build()]; + } + + return [RuleErrorBuilder::message('Regular property fetch detected')->identifier('')->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 f4a803df63..d3196aba89 100644 --- a/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php @@ -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-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-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-8074.php b/tests/PHPStan/Rules/Properties/data/bug-8074.php new file mode 100644 index 0000000000..7bedabd03b --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-8074.php @@ -0,0 +1,103 @@ += 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-8412.php b/tests/PHPStan/Rules/Properties/data/bug-8412.php new file mode 100644 index 0000000000..2fdc39dbbf --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-8412.php @@ -0,0 +1,25 @@ += 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/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..aa42587eab 100644 --- a/tests/PHPStan/Rules/Properties/data/missing-property-typehint.php +++ b/tests/PHPStan/Rules/Properties/data/missing-property-typehint.php @@ -93,3 +93,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/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-type-after-unset.php b/tests/PHPStan/Rules/Properties/data/property-type-after-unset.php new file mode 100644 index 0000000000..5c2cee2758 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/property-type-after-unset.php @@ -0,0 +1,40 @@ + */ + 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/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/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/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 @@ +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 36ad121dd7..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, ], [ @@ -441,4 +441,46 @@ public function testBug7292(): void $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 613770aead..4781fa1f18 100644 --- a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php +++ b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php @@ -368,4 +368,20 @@ public function testBug8084(): void $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/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 index 36a101f133..492ce244e7 100644 --- a/tests/PHPStan/Rules/Variables/data/bug-393.php +++ b/tests/PHPStan/Rules/Variables/data/bug-393.php @@ -1,6 +1,6 @@ &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{id: null, text: null, answer: null})&hasOffsetValue('SurveyInvitation', array)", $review); + assertType("non-empty-array&hasOffsetValue('Review', array)&hasOffsetValue('SurveyInvitation', array)", $review); } assertType('array', $review); if (array_key_exists('User', $review['Review'])) { @@ -42,7 +42,7 @@ function () { $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', mixed)&non-empty-array", $review); + 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-9126.php b/tests/PHPStan/Rules/Variables/data/bug-9126.php new file mode 100644 index 0000000000..7a5948d232 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-9126.php @@ -0,0 +1,23 @@ +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/data/scope-function-call-stack.php b/tests/PHPStan/Rules/data/scope-function-call-stack.php new file mode 100644 index 0000000000..700494fa88 --- /dev/null +++ b/tests/PHPStan/Rules/data/scope-function-call-stack.php @@ -0,0 +1,14 @@ += 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/CallableTypeTest.php b/tests/PHPStan/Type/CallableTypeTest.php index 1f6e0dc379..eec339aead 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,14 @@ 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(), + ], ]; } diff --git a/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php b/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php index 230c094b6f..0b8bff2efb 100644 --- a/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php @@ -111,4 +111,21 @@ public function testDegradedArrayIsNotAlwaysOversized(): void $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/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 75c9c0d7fe..70207aad08 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; @@ -153,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 { @@ -227,6 +332,7 @@ public function dataAccepts(): array /** * @dataProvider dataAccepts + * @dataProvider dataTypeProjections */ public function testAccepts( Type $acceptingType, @@ -379,6 +485,36 @@ 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, [ @@ -606,6 +742,36 @@ 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, [ @@ -914,6 +1080,36 @@ public function dataGetReferencedTypeArguments(): array ), ], ], + '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, [ @@ -1008,6 +1204,36 @@ public function dataGetReferencedTypeArguments(): array ), ], ], + '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(), + ), + ], + ], ]; } 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-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 @@ + [ + 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(), + ], ]; } @@ -486,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(), + ], ]; } @@ -638,4 +669,26 @@ public function testGetEnumCases( } } + 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/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index c19f93d1aa..76b0074909 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Type; +use Bug9006\TestInterface; use CheckTypeFunctionCall\FinalClassWithMethodExists; use CheckTypeFunctionCall\FinalClassWithPropertyExists; use Closure; @@ -12,7 +13,7 @@ use Exception; use InvalidArgumentException; use Iterator; -use NonExistantClass; +use ObjectShapesAcceptance\ClassWithFooIntProperty; use PHPStan\Fixture\FinalClass; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\Accessory\AccessoryLiteralStringType; @@ -2148,6 +2149,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( @@ -2332,86 +2345,182 @@ public function dataUnion(): iterable 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 ThisType( - $reflectionProvider->getClass(\TraitInstanceOf\ATrait1Class::class), // phpcs:ignore - null, - $reflectionProvider->getClass(\TraitInstanceOf\Trait1::class), // phpcs:ignore - ), - new ThisType( - $reflectionProvider->getClass(\TraitInstanceOf\FinalOther::class), // phpcs:ignore - null, - null, - ), + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(Traversable::class), ], UnionType::class, - '$this(TraitInstanceOf\ATrait1Class)|$this(TraitInstanceOf\FinalOther)', + 'object{foo: int}|Traversable', ]; yield [ [ - new ThisType( - $reflectionProvider->getClass(\TraitInstanceOf\FinalOther::class), // phpcs:ignore - null, - $reflectionProvider->getClass(\TraitInstanceOf\Trait1::class), // phpcs:ignore - ), - new ThisType( - $reflectionProvider->getClass(\TraitInstanceOf\WithoutFoo::class), // phpcs:ignore - null, - null, - ), + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectType(\ObjectShape\Foo::class), ], UnionType::class, - '$this(TraitInstanceOf\FinalOther)|$this(TraitInstanceOf\WithoutFoo)', + 'ObjectShape\Foo|object{foo: int}', ]; yield [ [ - new ThisType( - $reflectionProvider->getClass(\TraitInstanceOf\FinalTrait2Class::class), // phpcs:ignore - null, - $reflectionProvider->getClass(\TraitInstanceOf\Trait2::class), // phpcs:ignore - ), - new ObjectType(\TraitInstanceOf\ATrait1Class::class), // phpcs:ignore + 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, - '$this(TraitInstanceOf\FinalTrait2Class)|TraitInstanceOf\ATrait1Class', + 'ObjectShapesAcceptance\FinalClass|object{foo: int}', ]; yield [ [ - new ThisType( - $reflectionProvider->getClass(\TraitInstanceOf\FinalTrait2Class::class), // phpcs:ignore - null, - $reflectionProvider->getClass(\TraitInstanceOf\Trait2::class), // phpcs:ignore - ), - new ObjectType(\TraitInstanceOf\FinalOther::class), // phpcs:ignore + 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, - '$this(TraitInstanceOf\FinalTrait2Class)|TraitInstanceOf\FinalOther', + 'array{a?: true, b: true}|array{a?: true, c?: true}', ]; + yield [ [ - new ThisType( - $reflectionProvider->getClass(\TraitInstanceOf\FinalTrait2Class::class), // phpcs:ignore - new ObjectType(\TraitInstanceOf\FinalOther::class), // phpcs:ignore - $reflectionProvider->getClass(\TraitInstanceOf\Trait2::class), // phpcs:ignore - ), - new ObjectType(\TraitInstanceOf\FinalOther::class), // phpcs:ignore + 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, - '$this(TraitInstanceOf\FinalTrait2Class~TraitInstanceOf\FinalOther)|TraitInstanceOf\FinalOther', + 'array{a?: true, b: true}|(array{a?: true, c?: true}&non-empty-array)', ]; + yield [ [ - new ThisType( - $reflectionProvider->getClass(\TraitInstanceOf\FinalTrait2Class::class), // phpcs:ignore - new ObjectType(\TraitInstanceOf\FinalOther::class), // phpcs:ignore - $reflectionProvider->getClass(\TraitInstanceOf\Trait2::class), // phpcs:ignore - ), - new ObjectType(NonExistantClass::class), // phpcs:ignore // @phpstan-ignore-line + new EnumCaseObjectType('PHPStan\Fixture\AnotherTestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\AnotherTestEnum', 'TWO'), + new ObjectType('PHPStan\Fixture\TestEnumInterface'), ], UnionType::class, - '$this(TraitInstanceOf\FinalTrait2Class~TraitInstanceOf\FinalOther)|NonExistantClass', + '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', ]; } @@ -3865,118 +3974,193 @@ public function dataIntersect(): iterable IntersectionType::class, 'array&oversized-array', ]; - yield [ [ - new ThisType( - $reflectionProvider->getClass(\TraitInstanceOf\ATrait1Class::class), // phpcs:ignore - new ObjectType(\TraitInstanceOf\WithoutFoo::class), // phpcs:ignore - $reflectionProvider->getClass(\TraitInstanceOf\Trait1::class), // phpcs:ignore - ), - new ThisType( - $reflectionProvider->getClass(\TraitInstanceOf\WithoutFoo::class), // phpcs:ignore - null, - null, - ), + new ObjectType(TestInterface::class), + new ClosureType([], new MixedType(), false), ], NeverType::class, '*NEVER*=implicit', ]; yield [ [ - new ThisType( - $reflectionProvider->getClass(\TraitInstanceOf\FinalOther::class), // phpcs:ignore - new ObjectType(\TraitInstanceOf\WithoutFoo::class), // phpcs:ignore - $reflectionProvider->getClass(\TraitInstanceOf\Trait1::class), // phpcs:ignore - ), - new ObjectType(\TraitInstanceOf\WithoutFoo::class), // phpcs:ignore + new ObjectType(TestInterface::class), + new ObjectType(Closure::class), ], NeverType::class, '*NEVER*=implicit', ]; yield [ [ - new ThisType( - $reflectionProvider->getClass(\TraitInstanceOf\ATrait1Class::class), // phpcs:ignore - null, - $reflectionProvider->getClass(\TraitInstanceOf\Trait1::class), // phpcs:ignore - ), - new ObjectType(\TraitInstanceOf\FinalTrait2Class::class), // phpcs:ignore + new ObjectShapeType([], []), + new ObjectWithoutClassType(), ], - NeverType::class, - '*NEVER*=implicit', + ObjectShapeType::class, + 'object{}', ]; yield [ [ - new ThisType( - $reflectionProvider->getClass(\TraitInstanceOf\ATrait1Class::class), // phpcs:ignore - null, - $reflectionProvider->getClass(\TraitInstanceOf\Trait1::class), // phpcs:ignore - ), - new ObjectType(\TraitInstanceOf\FinalOther::class), // phpcs:ignore + 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 ThisType( - $reflectionProvider->getClass(\TraitInstanceOf\FinalOther::class), // phpcs:ignore - null, - $reflectionProvider->getClass(\TraitInstanceOf\Trait1::class), // phpcs:ignore - ), - new ObjectType(\TraitInstanceOf\WithoutFoo::class), // phpcs:ignore + new ObjectShapeType(['foo' => new IntegerType()], []), + new ObjectShapeType(['foo' => new IntegerType()], []), ], - IntersectionType::class, - '$this(TraitInstanceOf\FinalOther)&TraitInstanceOf\WithoutFoo', + ObjectShapeType::class, + 'object{foo: int}', ]; yield [ [ - new ThisType( - $reflectionProvider->getClass(\TraitInstanceOf\ATrait1Class::class), // phpcs:ignore - null, - $reflectionProvider->getClass(\TraitInstanceOf\Trait1::class), // phpcs:ignore - ), - new ObjectType(\TraitInstanceOf\Trait2::class), // phpcs:ignore + 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 ThisType( - $reflectionProvider->getClass(\TraitInstanceOf\FinalTrait2Class::class), // phpcs:ignore - null, - $reflectionProvider->getClass(\TraitInstanceOf\Trait2::class), // phpcs:ignore - ), - new ObjectType(\TraitInstanceOf\ATrait1Class::class), // phpcs:ignore + 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, - '$this(TraitInstanceOf\FinalTrait2Class)&TraitInstanceOf\ATrait1Class', + 'object{foo: int}&Traversable', ]; yield [ [ - new ThisType( - $reflectionProvider->getClass(\TraitInstanceOf\FinalTrait2Class::class), // phpcs:ignore - null, - $reflectionProvider->getClass(\TraitInstanceOf\Trait2::class), // phpcs:ignore - ), - new ObjectType(\TraitInstanceOf\FinalOther::class), // phpcs:ignore + 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 ThisType( - $reflectionProvider->getClass(\TraitInstanceOf\FinalTrait2Class::class), // phpcs:ignore - new ObjectType(\TraitInstanceOf\FinalOther::class), // phpcs:ignore - $reflectionProvider->getClass(\TraitInstanceOf\Trait2::class), // phpcs:ignore - ), - new ObjectType(NonExistantClass::class), // phpcs:ignore // @phpstan-ignore-line + 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, - '$this(TraitInstanceOf\FinalTrait2Class~TraitInstanceOf\FinalOther)&NonExistantClass', + 'array{a?: true, c?: true}&non-empty-array', ]; } @@ -4513,6 +4697,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}', + ], ]; } 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 bb9c59a270..cd0ee180cc 100644 --- a/tests/PHPStan/Type/UnionTypeTest.php +++ b/tests/PHPStan/Type/UnionTypeTest.php @@ -21,6 +21,7 @@ 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; @@ -32,6 +33,7 @@ use function array_reverse; use function get_class; use function sprintf; +use const PHP_VERSION_ID; class UnionTypeTest extends PHPStanTestCase { @@ -715,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([ @@ -740,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', ], [ @@ -761,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', ], [ @@ -782,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', ], [ @@ -803,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', ], [ @@ -823,6 +831,7 @@ public function dataDescribe(): array ]), ), 'array{int, bool, float}|array{string}', + 'array{int, bool, float}|array{string}', 'array', ], [ @@ -835,6 +844,7 @@ public function dataDescribe(): array ]), ), 'array{}|array{foooo: \'barrr\'}', + 'array{}|array{foooo: \'barrr\'}', 'array', ], [ @@ -846,6 +856,7 @@ public function dataDescribe(): array ]), ), 'int|numeric-string', + 'int|numeric-string', 'int|string', ], [ @@ -855,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( @@ -866,6 +878,7 @@ public function dataDescribe(): array ), new NullType(), ), + 'TFoo of int (class foo, parameter)|null', '(TFoo of int)|null', '(TFoo of int)|null', ], @@ -879,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', ], @@ -892,6 +906,7 @@ public function dataDescribe(): array ), new NullType(), ), + 'TFoo (class foo, parameter)|null', 'TFoo|null', 'TFoo|null', ], @@ -910,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', + ], ]; } @@ -921,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), @@ -999,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(), @@ -1184,7 +1291,6 @@ public function dataAccepts(): array ]), TrinaryLogic::createYes(), ], - ]; } 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 @@ bar; + } + } 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 @@ +