diff --git a/.editorconfig b/.editorconfig index 5d66bc427b..92ecb85c25 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,7 +6,7 @@ insert_final_newline = true charset = utf-8 trim_trailing_whitespace = true -[*.{php,phpt}] +[*.{php,phpt,stub}] indent_style = tab indent_size = 4 diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index d04f75155f..0000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,17 +0,0 @@ -version: 2 -updates: -- package-ecosystem: composer - directory: "/build-cs" - schedule: - interval: weekly - open-pull-requests-limit: 10 -- package-ecosystem: composer - directory: "/compiler" - schedule: - interval: weekly - open-pull-requests-limit: 10 -- package-ecosystem: github-actions - directory: "/" - schedule: - interval: monthly - open-pull-requests-limit: 10 diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000000..e23901f133 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,33 @@ +{ + "extends": [ + "config:base", + "schedule:weekly" + ], + "rangeStrategy": "update-lockfile", + "packageRules": [ + { + "matchPackagePatterns": ["*"], + "enabled": false + }, + { + "matchPaths": ["+(composer.json)"], + "enabled": true, + "groupName": "root-composer" + }, + { + "matchPaths": ["build-cs/**"], + "enabled": true, + "groupName": "build-cs" + }, + { + "matchPaths": ["compiler/**"], + "enabled": true, + "groupName": "compiler" + }, + { + "matchPaths": [".github/**"], + "enabled": true, + "groupName": "github-actions" + } + ] +} diff --git a/.github/workflows/backward-compatibility.yml b/.github/workflows/backward-compatibility.yml index 1e60de1f30..b1bdee7855 100644 --- a/.github/workflows/backward-compatibility.yml +++ b/.github/workflows/backward-compatibility.yml @@ -6,16 +6,17 @@ on: pull_request: push: branches: - - "master" + - "**" env: - COMPOSER_ROOT_VERSION: "0.12.x-dev" + COMPOSER_ROOT_VERSION: "1.5.x-dev" jobs: backward-compatibility: name: "Backward Compatibility" runs-on: "ubuntu-latest" + timeout-minutes: 60 steps: - name: "Checkout" @@ -30,13 +31,13 @@ jobs: php-version: "8.0" - name: "Install dependencies" - run: "composer install --no-dev --no-interaction --no-progress --no-suggest" + run: "composer install --no-dev --no-interaction --no-progress" - name: "Install BackwardCompatibilityCheck" run: | composer global config minimum-stability dev composer global config prefer-stable true - composer global require --dev ondrejmirtes/backward-compatibility-check:^5.0.7 + composer global require --dev ondrejmirtes/backward-compatibility-check:^5.0.8 - name: "Check" run: "$(composer global config bin-dir --absolute)/roave-backward-compatibility-check" diff --git a/.github/workflows/compiler-tests.yml b/.github/workflows/compiler-tests.yml index dd87983600..3333d665fe 100644 --- a/.github/workflows/compiler-tests.yml +++ b/.github/workflows/compiler-tests.yml @@ -6,16 +6,17 @@ on: pull_request: push: branches: - - "master" + - "**" env: - COMPOSER_ROOT_VERSION: "0.12.x-dev" + COMPOSER_ROOT_VERSION: "1.5.x-dev" jobs: compiler-tests: name: "Compiler Tests" runs-on: "ubuntu-latest" + timeout-minutes: 60 steps: - name: "Checkout" @@ -28,10 +29,7 @@ jobs: php-version: "8.0" - name: "Install dependencies" - run: "composer install --no-dev --no-interaction --no-progress --no-suggest" - - - name: "Transform source code" - run: php bin/transform-source.php + run: "composer install --no-interaction --no-progress" - name: "Tests" run: | @@ -44,5 +42,26 @@ jobs: - uses: actions/upload-artifact@v2 with: - name: phpstan.phar + name: phar-file path: tmp/phpstan.phar + + integration-tests: + if: github.event_name == 'pull_request' + needs: compiler-tests + uses: phpstan/phpstan/.github/workflows/integration-tests.yml@1.5.x + with: + ref: 1.5.x + + extension-tests: + if: github.event_name == 'pull_request' + needs: compiler-tests + uses: phpstan/phpstan/.github/workflows/extension-tests.yml@1.5.x + with: + ref: 1.5.x + + other-tests: + if: github.event_name == 'pull_request' + needs: compiler-tests + uses: phpstan/phpstan/.github/workflows/other-tests.yml@1.5.x + with: + ref: 1.5.x diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 649cfc9cfc..2c7ff7add9 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -4,24 +4,29 @@ name: "E2E Tests" on: pull_request: + paths-ignore: + - 'compiler/**' push: branches: - - "master" + - "**" + paths-ignore: + - 'compiler/**' env: - COMPOSER_ROOT_VERSION: "0.12.x-dev" + COMPOSER_ROOT_VERSION: "1.5.x-dev" jobs: result-cache-e2e-tests: name: "Result cache E2E tests" runs-on: ${{ matrix.operating-system }} + timeout-minutes: 60 strategy: fail-fast: false matrix: php-version: - - "7.4" + - "8.0" operating-system: [ubuntu-latest, windows-latest] steps: @@ -37,7 +42,7 @@ jobs: ini-values: memory_limit=256M - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" + run: "composer install --no-interaction --no-progress" - name: "Tests" run: | @@ -46,6 +51,7 @@ jobs: e2e-tests: name: "E2E tests" runs-on: "ubuntu-latest" + timeout-minutes: 60 strategy: matrix: @@ -68,12 +74,12 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - php-version: "7.4" + php-version: "8.0" tools: ${{ matrix.tools }} extensions: ${{ matrix.extensions }} - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" + run: "composer install --no-interaction --no-progress" - name: "Test" run: ${{ matrix.script }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 22c3bd4f36..0d64be5317 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,15 +6,16 @@ on: pull_request: push: branches: - - "master" + - "**" env: - COMPOSER_ROOT_VERSION: "0.12.x-dev" + COMPOSER_ROOT_VERSION: "1.5.x-dev" jobs: lint: name: "Lint" runs-on: "ubuntu-latest" + timeout-minutes: 60 strategy: fail-fast: false @@ -25,6 +26,7 @@ jobs: - "7.3" - "7.4" - "8.0" + - "8.1" steps: - name: "Checkout" @@ -40,11 +42,25 @@ jobs: run: "composer validate" - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" + run: "composer install --no-interaction --no-progress" + + - name: "Install PHP for code transform" + if: matrix.php-version != '8.0' && matrix.php-version != '8.1' + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: 8.0 - name: "Transform source code" - if: matrix.php-version != '7.4' && matrix.php-version != '8.0' - run: php bin/transform-source.php + if: matrix.php-version != '8.0' && matrix.php-version != '8.1' + run: "build/transform-source ${{ matrix.php-version }}" + + - name: "Reinstall matrix PHP version" + if: matrix.php-version != '8.0' && matrix.php-version != '8.1' + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" - name: "Lint" run: "make lint" @@ -53,6 +69,7 @@ jobs: name: "Coding Standard" runs-on: "ubuntu-latest" + timeout-minutes: 60 strategy: matrix: @@ -73,7 +90,7 @@ jobs: run: "composer validate" - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" + run: "composer install --no-interaction --no-progress" - name: "Lint" run: "make lint" @@ -85,6 +102,7 @@ jobs: name: "Dependency Analysis" runs-on: "ubuntu-latest" + timeout-minutes: 60 strategy: matrix: @@ -102,7 +120,7 @@ jobs: php-version: "${{ matrix.php-version }}" - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" + run: "composer install --no-interaction --no-progress" - name: "Composer Require Checker" run: "make composer-require-checker" diff --git a/.github/workflows/merge-bot-pr.yml b/.github/workflows/merge-bot-pr.yml new file mode 100644 index 0000000000..aa8c91d70f --- /dev/null +++ b/.github/workflows/merge-bot-pr.yml @@ -0,0 +1,29 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions +# https://github.com/WyriHaximus/github-action-wait-for-status + +name: Merge bot PR +on: + pull_request: + types: + - opened +jobs: + automerge: + name: Automerge PRs + runs-on: ubuntu-latest + steps: + - name: 'Wait for status checks' + if: github.event.pull_request.user.login == 'phpstan-bot' + id: waitforstatuschecks + uses: "WyriHaximus/github-action-wait-for-status@v1" + with: + ignoreActions: automerge + checkInterval: 13 + env: + GITHUB_TOKEN: "${{ secrets.PHPSTAN_BOT_TOKEN }}" + - name: Merge Pull Request + uses: juliangruber/merge-pull-request-action@v1 + if: steps.waitforstatuschecks.outputs.status == 'success' + with: + github-token: "${{ secrets.PHPSTAN_BOT_TOKEN }}" + number: "${{ github.event.number }}" + method: rebase diff --git a/.github/workflows/phar.yml b/.github/workflows/phar.yml index a1a99ee8a6..3952d78e56 100644 --- a/.github/workflows/phar.yml +++ b/.github/workflows/phar.yml @@ -5,14 +5,17 @@ name: "Compile PHAR" on: push: branches: - - "master" + - "1.5.x" tags: - - '*' + - '1.5.*' + +concurrency: phar jobs: compile: name: "Compile PHAR" runs-on: "ubuntu-latest" + timeout-minutes: 60 steps: - name: "Checkout" @@ -27,13 +30,10 @@ jobs: php-version: "8.0" - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" + run: "composer install --no-interaction --no-progress" - name: "Install compiler dependencies" - run: "composer install --no-interaction --no-progress --no-suggest --working-dir=compiler" - - - name: "Transform source code" - run: php bin/transform-source.php + run: "composer install --no-interaction --no-progress --working-dir=compiler" - name: "Compile PHAR" run: php compiler/bin/compile @@ -53,6 +53,7 @@ jobs: repository: phpstan/phpstan path: phpstan-dist token: ${{ secrets.PAT }} + ref: 1.5.x - name: "cp PHAR" run: cp tmp/phpstan.phar phpstan-dist/phpstan.phar @@ -79,13 +80,13 @@ jobs: git config user.email "ondrej@mirtes.cz" && \ git config user.name "Ondrej Mirtes" - - name: "Commit PHAR - master" + - name: "Commit PHAR - development" working-directory: phpstan-dist if: "!startsWith(github.ref, 'refs/tags/')" run: | git add phpstan.phar phpstan.phar.asc && \ git commit -S -m "Updated PHPStan to commit ${{ github.event.after }}" -m "${{ steps.git-log.outputs.log }}" && \ - git push --quiet origin master + git push --quiet origin 1.5.x - name: "Commit PHAR - tag" working-directory: phpstan-dist @@ -93,6 +94,6 @@ jobs: run: | git add phpstan.phar phpstan.phar.asc && \ git commit -S -m "PHPStan ${GITHUB_REF#refs/tags/}" -m "${{ steps.git-log.outputs.log }}" && \ - git push --quiet origin master && \ + git push --quiet origin 1.5.x && \ git tag -s ${GITHUB_REF#refs/tags/} -m "${GITHUB_REF#refs/tags/}" && \ git push --quiet origin ${GITHUB_REF#refs/tags/} diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 7e829b1f7b..7a2e880070 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -4,17 +4,22 @@ name: "Static Analysis" on: pull_request: + paths-ignore: + - 'compiler/**' push: branches: - - "master" + - "**" + paths-ignore: + - 'compiler/**' env: - COMPOSER_ROOT_VERSION: "0.12.x-dev" + COMPOSER_ROOT_VERSION: "1.5.x-dev" jobs: static-analysis: name: "PHPStan" runs-on: ${{ matrix.operating-system }} + timeout-minutes: 60 strategy: fail-fast: false @@ -25,6 +30,7 @@ jobs: - "7.3" - "7.4" - "8.0" + - "8.1" operating-system: [ubuntu-latest, windows-latest] script: - "make phpstan" @@ -43,15 +49,32 @@ jobs: extensions: mbstring - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" + run: "composer install --no-interaction --no-progress" - - name: "Downgrade PHPUnit" - if: matrix.php-version == '7.1' || matrix.php-version == '7.2' - run: "composer require --dev phpunit/phpunit:^7.5.20 brianium/paratest:^4.0 --update-with-dependencies" + - name: "Install PHP for code transform" + if: matrix.php-version != '8.0' && matrix.php-version != '8.1' + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: 8.0 + extensions: mbstring - name: "Transform source code" - if: matrix.php-version != '7.4' && matrix.php-version != '8.0' - run: php bin/transform-source.php + if: matrix.php-version != '8.0' && matrix.php-version != '8.1' + shell: bash + run: "build/transform-source ${{ matrix.php-version }}" + + - name: "Reinstall matrix PHP version" + if: matrix.php-version != '8.0' && matrix.php-version != '8.1' + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + extensions: mbstring + + - name: "Downgrade PHPUnit" + if: matrix.php-version == '7.1' || matrix.php-version == '7.2' + run: "composer require --dev phpunit/phpunit:^7.5.20 brianium/paratest:^4.0 --update-with-dependencies --ignore-platform-reqs" - name: "PHPStan" run: ${{ matrix.script }} @@ -59,12 +82,13 @@ jobs: static-analysis-with-result-cache: name: "PHPStan with result cache" - runs-on: ubuntu-latest + runs-on: "ubuntu-latest" + timeout-minutes: 60 strategy: matrix: php-version: - - "7.4" + - "8.0" steps: - name: "Checkout" @@ -78,7 +102,7 @@ jobs: extensions: mbstring - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" + run: "composer install --no-interaction --no-progress" - name: "Cache Result cache" uses: actions/cache@v2 @@ -105,11 +129,12 @@ jobs: name: "Generate baseline" runs-on: "ubuntu-latest" + timeout-minutes: 60 strategy: matrix: php-version: - - "7.4" + - "8.0" steps: - name: "Checkout" @@ -122,7 +147,7 @@ jobs: php-version: "${{ matrix.php-version }}" - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" + run: "composer install --no-interaction --no-progress" - name: "Generate baseline" run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 63439d983c..cfdbc37dfe 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,17 +4,22 @@ name: "Tests" on: pull_request: + paths-ignore: + - 'compiler/**' push: branches: - - "master" + - "**" + paths-ignore: + - 'compiler/**' env: - COMPOSER_ROOT_VERSION: "0.12.x-dev" + COMPOSER_ROOT_VERSION: "1.5.x-dev" jobs: tests: name: "Tests" runs-on: ${{ matrix.operating-system }} + timeout-minutes: 60 strategy: fail-fast: false @@ -23,6 +28,7 @@ jobs: - "7.3" - "7.4" - "8.0" + - "8.1" operating-system: [ ubuntu-latest, windows-latest ] script: - "make tests" @@ -43,11 +49,30 @@ jobs: ini-values: memory_limit=640M - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" + run: "composer install --no-interaction --no-progress" + + - name: "Install PHP for code transform" + if: matrix.php-version != '8.0' && matrix.php-version != '8.1' + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: 8.0 + extensions: mbstring - name: "Transform source code" - if: matrix.php-version != '7.4' && matrix.php-version != '8.0' - run: php bin/transform-source.php + if: matrix.php-version != '8.0' && matrix.php-version != '8.1' + shell: bash + run: "build/transform-source ${{ matrix.php-version }}" + + - name: "Reinstall matrix PHP version" + if: matrix.php-version != '8.0' && matrix.php-version != '8.1' + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + tools: pecl + extensions: ds,mbstring + ini-values: memory_limit=640M - name: "Tests" run: "${{ matrix.script }}" @@ -55,6 +80,7 @@ jobs: tests-old-phpunit: name: "Tests with old PHPUnit" runs-on: ${{ matrix.operating-system }} + timeout-minutes: 60 strategy: fail-fast: false @@ -82,44 +108,29 @@ jobs: ini-values: memory_limit=640M - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" + run: "composer install --no-interaction --no-progress" - - name: "Downgrade PHPUnit" - run: "composer require --dev phpunit/phpunit:^7.5.20 brianium/paratest:^4.0 --update-with-dependencies" + - name: "Install PHP for code transform" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.0" - name: "Transform source code" - run: php bin/transform-source.php + shell: bash + run: "build/transform-source ${{ matrix.php-version }}" - - name: "Tests" - run: "${{ matrix.script }}" - - tests-code-coverage: - name: "Tests with code coverage" - - runs-on: "ubuntu-latest" - - steps: - - name: "Checkout" - uses: "actions/checkout@v2" - - - name: "Install PHP" + - name: "Reinstall matrix PHP version" uses: "shivammathur/setup-php@v2" with: - coverage: "pcov" - php-version: "7.4" + coverage: "none" + php-version: "${{ matrix.php-version }}" tools: pecl - extensions: ds + extensions: ds,mbstring + ini-values: memory_limit=640M - - name: "Install dependencies" - run: "composer install --no-interaction --no-progress --no-suggest" + - name: "Downgrade PHPUnit" + run: "composer require --dev phpunit/phpunit:^7.5.20 brianium/paratest:^4.0 --update-with-dependencies --ignore-platform-reqs" - name: "Tests" - run: | - php -dpcov.enabled=1 -dpcov.directory=. -dpcov.exclude="~vendor~" vendor/bin/phpunit - - - name: "Coveralls" - env: - COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - composer require twinh/php-coveralls --dev && \ - vendor/bin/php-coveralls --verbose --coverage_clover=tests/tmp/clover.xml --json_path=tests/tmp/coveralls-upload.json + run: "${{ matrix.script }}" diff --git a/.github/workflows/update-phpstorm-stubs.yml b/.github/workflows/update-phpstorm-stubs.yml new file mode 100644 index 0000000000..b1618055b0 --- /dev/null +++ b/.github/workflows/update-phpstorm-stubs.yml @@ -0,0 +1,50 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Update PhpStorm stubs" +on: + workflow_dispatch: + schedule: + # * is a special character in YAML so you have to quote this string + - cron: '0 0 * * 2' + +jobs: + update-phpstorm-stubs: + name: "Update PhpStorm stubs" + if: ${{ github.repository == 'phpstan/phpstan-src' }} + runs-on: "ubuntu-latest" + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + with: + ref: ${{ github.head_ref }} + fetch-depth: '0' + token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "8.0" + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + - name: "Checkout stubs" + uses: "actions/checkout@v2" + with: + path: "phpstorm-stubs" + repository: "jetbrains/phpstorm-stubs" + - name: "Update stubs" + run: "composer require jetbrains/phpstorm-stubs:dev-master#$(git -C phpstorm-stubs rev-parse HEAD)" + - name: "Remove stubs repo" + run: "rm -r phpstorm-stubs" + - name: "Update function metadata" + run: "./bin/generate-function-metadata.php" + - name: "Create Pull Request" + id: create-pr + uses: peter-evans/create-pull-request@v3 + with: + token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + branch-suffix: random + delete-branch: true + title: "Update PhpStorm stubs" + body: "Update PhpStorm stubs" + committer: "phpstan-bot " + commit-message: "Update PhpStorm stubs" diff --git a/.gitignore b/.gitignore index 625cace6ae..65cd70f464 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ /compiler/vendor /conf/config.local.yml /vendor -/.idea +/.idea/* +!.idea/icon.png /tests/tmp /tests/.phpunit.result.cache +tmp/.memory_limit diff --git a/.idea/icon.png b/.idea/icon.png new file mode 100644 index 0000000000..5f346e71c1 Binary files /dev/null and b/.idea/icon.png differ diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 162e250c6d..7ab0db920a 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,74 +1,134 @@ -# Contributor Code of Conduct + +# Contributor Covenant Code of Conduct ## Our Pledge -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, gender identity and expression, level of experience, -nationality, personal appearance, race, religion, or sexual identity and -orientation. +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. ## Our Standards -Examples of behavior that contributes to creating a positive environment -include: +Examples of behavior that contributes to a positive environment for our +community include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community -Examples of unacceptable behavior by participants include: +Examples of unacceptable behavior include: -* The use of sexualized language or imagery and unwelcome sexual attention or -advances -* Trolling, insulting/derogatory comments, and personal or political attacks +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission +* Publishing others' private information, such as a physical or email address, + without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting -## Our Responsibilities +## Enforcement Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. ## Scope -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project maintainer at . All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. +reported to the community leaders responsible for enforcement at +ondrej@mirtes.cz. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at [http://contributor-covenant.org/version/1/4][version] +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ diff --git a/Makefile b/Makefile index 0bfa9d747e..fa08492748 100644 --- a/Makefile +++ b/Makefile @@ -28,6 +28,8 @@ lint: --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 \ + --exclude tests/PHPStan/Rules/Classes/data/duplicate-enum-cases.php \ + --exclude tests/PHPStan/Rules/Classes/data/enum-sanity.php \ --exclude tests/PHPStan/Rules/Classes/data/extends-error.php \ --exclude tests/PHPStan/Rules/Classes/data/implements-error.php \ --exclude tests/PHPStan/Rules/Classes/data/interface-extends-error.php \ @@ -41,7 +43,13 @@ 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 \ - src tests compiler/src + --exclude tests/PHPStan/Rules/Properties/data/read-only-property.php \ + --exclude tests/PHPStan/Rules/Properties/data/overriding-property.php \ + --exclude tests/PHPStan/Rules/Constants/data/overriding-final-constant.php \ + --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 \ + src tests cs: composer install --working-dir build-cs && php build-cs/vendor/bin/phpcs diff --git a/README.md b/README.md index 6692591ca7..b64d557a42 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # PHPStan - PHP Static Analysis Tool -[![Build](https://github.com/phpstan/phpstan-src/workflows/Build/badge.svg)](https://github.com/phpstan/phpstan-src/actions) -[![Coverage Status](https://coveralls.io/repos/github/phpstan/phpstan-src/badge.svg)](https://coveralls.io/github/phpstan/phpstan-src) +[![Build](https://github.com/phpstan/phpstan-src/workflows/Tests/badge.svg)](https://github.com/phpstan/phpstan-src/actions) [![PHPStan Enabled](https://img.shields.io/badge/PHPStan-enabled-brightgreen.svg?style=flat)](https://github.com/phpstan/phpstan) --- @@ -14,9 +13,9 @@ Any contributions are welcome. ### Building -PHPStan's source code is developed on PHP 7.4. For distribution in `phpstan/phpstan` package and as a PHAR file, the source code is transformed to run on PHP 7.1 and higher. +PHPStan's source code is developed on PHP 8.0. For distribution in `phpstan/phpstan` package and as a PHAR file, the source code is transformed to run on PHP 7.1 and higher. -Initially you need to run `composer install`, or `composer update` in case you aren't working in a directory which was built before. +Initially you need to run `composer install` in case you aren't working in a directory which was built before. Afterwards you can either run the whole build including linting and coding standards using @@ -40,8 +39,6 @@ To detect code style issues, run: make cs ``` -This requires PHP 7.4. On older versions the build target will be skipped and succeed silently. - And then to fix code style, run: ```bash diff --git a/bin/functionMetadata_original.php b/bin/functionMetadata_original.php index b7505a6f6f..8004c6dcda 100644 --- a/bin/functionMetadata_original.php +++ b/bin/functionMetadata_original.php @@ -61,8 +61,46 @@ 'bcmod' => ['hasSideEffects' => false], 'bcmul' => ['hasSideEffects' => false], // continue functionMap.php, line 424 + 'chgrp' => ['hasSideEffects' => true], + 'chmod' => ['hasSideEffects' => true], + 'chown' => ['hasSideEffects' => true], + 'copy' => ['hasSideEffects' => true], 'count' => ['hasSideEffects' => false], + 'fclose' => ['hasSideEffects' => true], + 'fflush' => ['hasSideEffects' => true], + 'fgetc' => ['hasSideEffects' => true], + 'fgetcsv' => ['hasSideEffects' => true], + 'fgets' => ['hasSideEffects' => true], + 'fgetss' => ['hasSideEffects' => true], + 'file_put_contents' => ['hasSideEffects' => true], + 'flock' => ['hasSideEffects' => true], + 'fopen' => ['hasSideEffects' => true], + 'fpassthru' => ['hasSideEffects' => true], + 'fputcsv' => ['hasSideEffects' => true], + 'fputs' => ['hasSideEffects' => true], + 'fread' => ['hasSideEffects' => true], + 'fscanf' => ['hasSideEffects' => true], + 'fseek' => ['hasSideEffects' => true], + 'ftruncate' => ['hasSideEffects' => true], + 'fwrite' => ['hasSideEffects' => true], + 'lchgrp' => ['hasSideEffects' => true], + 'lchown' => ['hasSideEffects' => true], + 'link' => ['hasSideEffects' => true], + 'mkdir' => ['hasSideEffects' => true], + 'move_uploaded_file' => ['hasSideEffects' => true], + 'pclose' => ['hasSideEffects' => true], + 'popen' => ['hasSideEffects' => true], + 'readfile' => ['hasSideEffects' => true], + 'rename' => ['hasSideEffects' => true], + 'rewind' => ['hasSideEffects' => true], + 'rmdir' => ['hasSideEffects' => true], 'sprintf' => ['hasSideEffects' => false], + 'symlink' => ['hasSideEffects' => true], + 'tempnam' => ['hasSideEffects' => true], + 'tmpfile' => ['hasSideEffects' => true], + 'touch' => ['hasSideEffects' => true], + 'umask' => ['hasSideEffects' => true], + 'unlink' => ['hasSideEffects' => true], // random functions, do not have side effects but are not deterministic 'mt_rand' => ['hasSideEffects' => true], diff --git a/bin/generate-function-metadata.php b/bin/generate-function-metadata.php index a7e4ebd35a..a3779dda53 100755 --- a/bin/generate-function-metadata.php +++ b/bin/generate-function-metadata.php @@ -29,7 +29,7 @@ public function enterNode(Node $node) foreach ($attrGroup->attrs as $attr) { if ($attr->name->toString() === \JetBrains\PhpStorm\Pure::class) { $this->functions[] = $node->namespacedName->toLowerString(); - break; + break 2; } } } @@ -45,7 +45,7 @@ public function enterNode(Node $node) foreach ($attrGroup->attrs as $attr) { if ($attr->name->toString() === \JetBrains\PhpStorm\Pure::class) { $this->methods[] = sprintf('%s::%s', $className, $node->name->toString()); - break; + break 2; } } } @@ -71,6 +71,14 @@ public function enterNode(Node $node) foreach ($visitor->functions as $functionName) { if (array_key_exists($functionName, $metadata)) { if ($metadata[$functionName]['hasSideEffects']) { + if (in_array($functionName, [ + 'mt_rand', + 'rand', + 'random_bytes', + 'random_int', + ], true)) { + continue; + } throw new \PHPStan\ShouldNotHappenException($functionName); } } diff --git a/bin/phpstan b/bin/phpstan index de25ec2c0a..2c06a78841 100755 --- a/bin/phpstan +++ b/bin/phpstan @@ -4,7 +4,6 @@ use PHPStan\Command\AnalyseCommand; use PHPStan\Command\ClearResultCacheCommand; use PHPStan\Command\FixerWorkerCommand; -use PHPStan\Command\DumpDependenciesCommand; use PHPStan\Command\WorkerCommand; use Symfony\Component\Console\Helper\ProgressBar; @@ -28,6 +27,13 @@ use Symfony\Component\Console\Helper\ProgressBar; if ( !array_key_exists('e88992873b7765f9b5710cab95ba5dd7', $composerAutoloadFiles) || !array_key_exists('3e76f7f02b41af8cea96018933f6b7e3', $composerAutoloadFiles) + || !array_key_exists('a4a119a56e50fbb293281d9a48007e0e', $composerAutoloadFiles) + || !array_key_exists('0e6d7bf4a5811bfa5cf40c5ccd6fae6a', $composerAutoloadFiles) + || !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) ) { echo "Composer autoloader changed\n"; exit(1); @@ -38,6 +44,27 @@ use Symfony\Component\Console\Helper\ProgressBar; // fix unprefixed Hoa namespace - files already loaded 'e88992873b7765f9b5710cab95ba5dd7' => true, '3e76f7f02b41af8cea96018933f6b7e3' => true, + + // vendor/symfony/polyfill-php80/bootstrap.php + 'a4a119a56e50fbb293281d9a48007e0e' => true, + + // vendor/symfony/polyfill-mbstring/bootstrap.php + '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => true, + + // vendor/symfony/polyfill-intl-normalizer/bootstrap.php + 'e69f7f6ee287b969198c3c9d6777bd38' => true, + + // vendor/symfony/polyfill-php73/bootstrap.php + '0d59ee240a4cd96ddbb4ff164fccea4d' => true, + + // 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, ]; $autoloaderInWorkingDirectory = getcwd() . '/vendor/autoload.php'; @@ -81,7 +108,7 @@ use Symfony\Component\Console\Helper\ProgressBar; $version = 'Version unknown'; try { - $version = \Jean85\PrettyVersions::getVersion('phpstan/phpstan')->getPrettyVersion(); + $version = \Jean85\PrettyVersions::getVersion('phpstan/phpstan')->getPrettyVersion() ?: $version; } catch (\OutOfBoundsException $e) { } @@ -95,7 +122,6 @@ use Symfony\Component\Console\Helper\ProgressBar; $reversedComposerAutoloaderProjectPaths = array_reverse($composerAutoloaderProjectPaths); $application->add(new AnalyseCommand($reversedComposerAutoloaderProjectPaths)); - $application->add(new DumpDependenciesCommand($reversedComposerAutoloaderProjectPaths)); $application->add(new WorkerCommand($reversedComposerAutoloaderProjectPaths)); $application->add(new ClearResultCacheCommand($reversedComposerAutoloaderProjectPaths)); $application->add(new FixerWorkerCommand($reversedComposerAutoloaderProjectPaths)); diff --git a/bin/transform-source.php b/bin/transform-source.php deleted file mode 100755 index 8ecbefb023..0000000000 --- a/bin/transform-source.php +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env php -type === null) { - return null; - } - $docComment = $node->getDocComment(); - if ($docComment !== null) { - $node->type = null; - return $node; - } - - $node->setDocComment(new \PhpParser\Comment\Doc(sprintf('/** @var %s */', $this->printType($node->type)))); - $node->type = null; - - return $node; - } - - /** - * @param Identifier|Name|NullableType|UnionType $type - * @return string - */ - private function printType($type): string - { - if ($type instanceof NullableType) { - return $this->printType($type->type) . '|null'; - } - - if ($type instanceof UnionType) { - throw new \Exception('UnionType not yet supported'); - } - - if ($type instanceof Name) { - $name = $type->toString(); - if ($type->isFullyQualified()) { - return '\\' . $name; - } - - return $name; - } - - if ($type instanceof Identifier) { - return $type->name; - } - - throw new \Exception('Unsupported type class'); - } - -} - -(function () { - $dir = __DIR__ . '/../src'; - - $lexer = new Lexer\Emulative([ - 'usedAttributes' => [ - 'comments', - 'startLine', 'endLine', - 'startTokenPos', 'endTokenPos', - ], - ]); - $parser = new Parser\Php7($lexer, [ - 'useIdentifierNodes' => true, - 'useConsistentVariableNodes' => true, - 'useExpressionStatements' => true, - 'useNopStatements' => false, - ]); - $nameResolver = new NodeVisitor\NameResolver(null, [ - 'replaceNodes' => false - ]); - - $printer = new PrettyPrinter\Standard(); - - $traverser = new NodeTraverser(); - $traverser->addVisitor(new NodeVisitor\CloningVisitor()); - $traverser->addVisitor($nameResolver); - $traverser->addVisitor(new PhpPatcher($printer)); - - $it = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($dir), - RecursiveIteratorIterator::LEAVES_ONLY - ); - foreach ($it as $file) { - $fileName = $file->getPathname(); - if (!preg_match('/\.php$/', $fileName)) { - continue; - } - - $code = \PHPStan\File\FileReader::read($fileName); - $origStmts = $parser->parse($code); - $newCode = $printer->printFormatPreserving( - $traverser->traverse($origStmts), - $origStmts, - $lexer->getTokens() - ); - - \PHPStan\File\FileWriter::write($fileName, $newCode); - } -})(); diff --git a/build-cs/composer.json b/build-cs/composer.json index 562c29c0f4..7912119afa 100644 --- a/build-cs/composer.json +++ b/build-cs/composer.json @@ -2,7 +2,12 @@ "require-dev": { "consistence-community/coding-standard": "^3.11.0", "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", - "slevomat/coding-standard": "^6.3.0", + "slevomat/coding-standard": "^7.0.18", "squizlabs/php_codesniffer": "^3.5.3" + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } } } diff --git a/build-cs/composer.lock b/build-cs/composer.lock index 19ea198bf0..475b81fba0 100644 --- a/build-cs/composer.lock +++ b/build-cs/composer.lock @@ -4,35 +4,35 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2bd0f4453d373af44aff8ec8868f82e9", + "content-hash": "98d65cadfe6f694505bce09b80004cae", "packages": [], "packages-dev": [ { "name": "consistence-community/coding-standard", - "version": "3.11.0", + "version": "3.11.1", "source": { "type": "git", "url": "https://github.com/consistence-community/coding-standard.git", - "reference": "20f5c3673013be606a62ba0b6624f5c0e43bb64e" + "reference": "4632fead8c9ee8f50044fcbce9f66c797b34c0df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consistence-community/coding-standard/zipball/20f5c3673013be606a62ba0b6624f5c0e43bb64e", - "reference": "20f5c3673013be606a62ba0b6624f5c0e43bb64e", + "url": "https://api.github.com/repos/consistence-community/coding-standard/zipball/4632fead8c9ee8f50044fcbce9f66c797b34c0df", + "reference": "4632fead8c9ee8f50044fcbce9f66c797b34c0df", "shasum": "" }, "require": { "php": ">=7.4", - "slevomat/coding-standard": "~6.4", - "squizlabs/php_codesniffer": "~3.5.8" + "slevomat/coding-standard": "~7.0", + "squizlabs/php_codesniffer": "~3.6.0" }, "replace": { "consistence/coding-standard": "3.10.*" }, "require-dev": { "phing/phing": "2.16.4", - "php-parallel-lint/php-parallel-lint": "1.2.0", - "phpunit/phpunit": "9.5.2" + "php-parallel-lint/php-parallel-lint": "1.3.0", + "phpunit/phpunit": "9.5.4" }, "type": "library", "autoload": { @@ -69,33 +69,34 @@ "standard" ], "support": { - "source": "https://github.com/consistence-community/coding-standard/tree/3.11.0" + "issues": "https://github.com/consistence-community/coding-standard/issues", + "source": "https://github.com/consistence-community/coding-standard/tree/3.11.1" }, - "time": "2021-02-28T08:38:12+00:00" + "time": "2021-05-03T18:13:22+00:00" }, { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v0.7.1", + "version": "v0.7.2", "source": { "type": "git", "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", - "reference": "fe390591e0241955f22eb9ba327d137e501c771c" + "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/fe390591e0241955f22eb9ba327d137e501c771c", - "reference": "fe390591e0241955f22eb9ba327d137e501c771c", + "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", + "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", "shasum": "" }, "require": { "composer-plugin-api": "^1.0 || ^2.0", "php": ">=5.3", - "squizlabs/php_codesniffer": "^2.0 || ^3.0 || ^4.0" + "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" }, "require-dev": { "composer/composer": "*", - "phpcompatibility/php-compatibility": "^9.0", - "sensiolabs/security-checker": "^4.1.0" + "php-parallel-lint/php-parallel-lint": "^1.3.1", + "phpcompatibility/php-compatibility": "^9.0" }, "type": "composer-plugin", "extra": { @@ -116,6 +117,10 @@ "email": "franck.nijhof@dealerdirect.com", "homepage": "http://www.frenck.nl", "role": "Developer / IT Manager" + }, + { + "name": "Contributors", + "homepage": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer/graphs/contributors" } ], "description": "PHP_CodeSniffer Standards Composer Installer Plugin", @@ -127,6 +132,7 @@ "codesniffer", "composer", "installer", + "phpcbf", "phpcs", "plugin", "qa", @@ -141,41 +147,37 @@ "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" }, - "time": "2020-12-07T18:04:37+00:00" + "time": "2022-02-04T12:51:07+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "0.4.9", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "98a088b17966bdf6ee25c8a4b634df313d8aa531" + "reference": "dbc093d7af60eff5cd575d2ed761b15ed40bd08e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/98a088b17966bdf6ee25c8a4b634df313d8aa531", - "reference": "98a088b17966bdf6ee25c8a4b634df313d8aa531", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/dbc093d7af60eff5cd575d2ed761b15ed40bd08e", + "reference": "dbc093d7af60eff5cd575d2ed761b15ed40bd08e", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "require-dev": { - "consistence/coding-standard": "^3.5", - "ergebnis/composer-normalize": "^2.0.2", - "jakub-onderka/php-parallel-lint": "^0.9.2", - "phing/phing": "^2.16.0", + "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^0.12.26", - "phpstan/phpstan-strict-rules": "^0.12", - "phpunit/phpunit": "^6.3", - "slevomat/coding-standard": "^4.7.2", - "symfony/process": "^4.0" + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "symfony/process": "^5.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "0.4-dev" + "dev-master": "1.0-dev" } }, "autoload": { @@ -192,43 +194,43 @@ "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/master" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.2.0" }, - "time": "2020-08-03T20:32:43+00:00" + "time": "2021-09-16T20:46:02+00:00" }, { "name": "slevomat/coding-standard", - "version": "6.4.1", + "version": "7.0.19", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "696dcca217d0c9da2c40d02731526c1e25b65346" + "reference": "bef66a43815bbf9b5f49775e9ded3f7c6ba0cc37" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/696dcca217d0c9da2c40d02731526c1e25b65346", - "reference": "696dcca217d0c9da2c40d02731526c1e25b65346", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/bef66a43815bbf9b5f49775e9ded3f7c6ba0cc37", + "reference": "bef66a43815bbf9b5f49775e9ded3f7c6ba0cc37", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7", "php": "^7.1 || ^8.0", - "phpstan/phpdoc-parser": "0.4.5 - 0.4.9", - "squizlabs/php_codesniffer": "^3.5.6" + "phpstan/phpdoc-parser": "^1.0.0", + "squizlabs/php_codesniffer": "^3.6.2" }, "require-dev": { - "phing/phing": "2.16.3", - "php-parallel-lint/php-parallel-lint": "1.2.0", - "phpstan/phpstan": "0.12.48", - "phpstan/phpstan-deprecation-rules": "0.12.5", - "phpstan/phpstan-phpunit": "0.12.16", - "phpstan/phpstan-strict-rules": "0.12.5", - "phpunit/phpunit": "7.5.20|8.5.5|9.4.0" + "phing/phing": "2.17.2", + "php-parallel-lint/php-parallel-lint": "1.3.2", + "phpstan/phpstan": "1.4.6", + "phpstan/phpstan-deprecation-rules": "1.0.0", + "phpstan/phpstan-phpunit": "1.0.0", + "phpstan/phpstan-strict-rules": "1.1.0", + "phpunit/phpunit": "7.5.20|8.5.21|9.5.16" }, "type": "phpcodesniffer-standard", "extra": { "branch-alias": { - "dev-master": "6.x-dev" + "dev-master": "7.x-dev" } }, "autoload": { @@ -243,7 +245,7 @@ "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.", "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/6.4.1" + "source": "https://github.com/slevomat/coding-standard/tree/7.0.19" }, "funding": [ { @@ -255,20 +257,20 @@ "type": "tidelift" } ], - "time": "2020-10-05T12:39:37+00:00" + "time": "2022-03-01T18:01:41+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.5.8", + "version": "3.6.2", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "9d583721a7157ee997f235f327de038e7ea6dac4" + "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/9d583721a7157ee997f235f327de038e7ea6dac4", - "reference": "9d583721a7157ee997f235f327de038e7ea6dac4", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/5e4e71592f69da17871dba6e80dd51bce74a351a", + "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a", "shasum": "" }, "require": { @@ -311,7 +313,7 @@ "source": "https://github.com/squizlabs/PHP_CodeSniffer", "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" }, - "time": "2020-10-23T02:01:07+00:00" + "time": "2021-12-12T21:44:58+00:00" } ], "aliases": [], @@ -321,5 +323,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "2.0.0" + "plugin-api-version": "2.2.0" } diff --git a/build/PHPStan/Build/ServiceLocatorDynamicReturnTypeExtension.php b/build/PHPStan/Build/ServiceLocatorDynamicReturnTypeExtension.php index 8ad0e108c1..da67c961e6 100644 --- a/build/PHPStan/Build/ServiceLocatorDynamicReturnTypeExtension.php +++ b/build/PHPStan/Build/ServiceLocatorDynamicReturnTypeExtension.php @@ -7,8 +7,6 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -30,23 +28,20 @@ public function isMethodSupported(MethodReflection $methodReflection): bool public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type { - if (count($methodCall->args) === 0) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - } - $argType = $scope->getType($methodCall->args[0]->value); - if (!$argType instanceof ConstantStringType) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + if (count($methodCall->getArgs()) === 0) { + return ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType(); } - $type = new ObjectType($argType->getValue()); - if ($methodReflection->getName() === 'getByType' && count($methodCall->args) >= 2) { - $argType = $scope->getType($methodCall->args[1]->value); + $returnType = ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType(); + + if ($methodReflection->getName() === 'getByType' && count($methodCall->getArgs()) >= 2) { + $argType = $scope->getType($methodCall->getArgs()[1]->value); if ($argType instanceof ConstantBooleanType && $argType->getValue()) { - $type = TypeCombinator::addNull($type); + $returnType = TypeCombinator::addNull($returnType); } } - return $type; + return $returnType; } } diff --git a/build/baseline-32bit.neon b/build/baseline-32bit.neon new file mode 100644 index 0000000000..82adbae209 --- /dev/null +++ b/build/baseline-32bit.neon @@ -0,0 +1,6 @@ +parameters: + ignoreErrors: + - + message: "#^Parameter \\#1 \\$value of class PHPStan\\\\Type\\\\Constant\\\\ConstantIntegerType constructor expects int, float given\\.$#" + count: 2 + path: ../src/Analyser/MutatingScope.php diff --git a/build/baseline-7.4.neon b/build/baseline-7.4.neon index 0d3f438771..f1efeccdf6 100644 --- a/build/baseline-7.4.neon +++ b/build/baseline-7.4.neon @@ -8,20 +8,6 @@ parameters: 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\\\\DependencyInjection\\\\Type\\\\DirectDynamicReturnTypeExtensionRegistryProvider has an uninitialized property \\$broker\\. Give it default value or assign it in the constructor\\.$#" - count: 1 - path: ../src/DependencyInjection/Type/DirectDynamicReturnTypeExtensionRegistryProvider.php - - - - message: "#^Class PHPStan\\\\DependencyInjection\\\\Type\\\\DirectDynamicReturnTypeExtensionRegistryProvider has an uninitialized property \\$reflectionProvider\\. Give it default value or assign it in the constructor\\.$#" - count: 1 - path: ../src/DependencyInjection/Type/DirectDynamicReturnTypeExtensionRegistryProvider.php - - - - message: "#^Class PHPStan\\\\DependencyInjection\\\\Type\\\\DirectOperatorTypeSpecifyingExtensionRegistryProvider has an uninitialized property \\$broker\\. Give it default value or assign it in the constructor\\.$#" - count: 1 - path: ../src/DependencyInjection/Type/DirectOperatorTypeSpecifyingExtensionRegistryProvider.php - message: "#^Class PHPStan\\\\Parallel\\\\ParallelAnalyser has an uninitialized property \\$processPool\\. Give it default value or assign it in the constructor\\.$#" @@ -74,6 +60,11 @@ parameters: count: 1 path: ../src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php + - + message: "#^Class PHPStan\\\\Reflection\\\\BetterReflection\\\\SourceLocator\\\\CachingVisitor has an uninitialized property \\$contents\\. Give it default value or assign it in the constructor\\.$#" + count: 1 + path: ../src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php + - message: "#^Class PHPStan\\\\Reflection\\\\BetterReflection\\\\SourceLocator\\\\CachingVisitor has an uninitialized property \\$classNodes\\. 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 17ff4b0ca8..7d6f91d1c8 100644 --- a/build/baseline-8.0.neon +++ b/build/baseline-8.0.neon @@ -11,7 +11,7 @@ parameters: path: ../src/Type/Php/MbFunctionsReturnTypeExtension.php - - message: "#^Call to function is_array\\(\\) with array\\ will always evaluate to true\\.$#" + message: "#^Strict comparison using \\=\\=\\= between non-empty-array and false will always evaluate to false\\.$#" count: 1 path: ../src/Type/Php/StrSplitFunctionReturnTypeExtension.php diff --git a/build/baseline-8.1.neon b/build/baseline-8.1.neon new file mode 100644 index 0000000000..fdd42f9d3d --- /dev/null +++ b/build/baseline-8.1.neon @@ -0,0 +1,22 @@ +parameters: + ignoreErrors: + - + message: "#^Call to function method_exists\\(\\) with ReflectionClassConstant and 'isFinal' will always evaluate to true\\.$#" + count: 1 + path: ../src/Reflection/ClassConstantReflection.php + + - + message: "#^Call to function method_exists\\(\\) with ReflectionClass and 'isEnum' will always evaluate to true\\.$#" + count: 1 + path: ../src/Reflection/ClassReflection.php + + - + message: "#^Call to function method_exists\\(\\) with ReflectionMethod and 'getTentativeReturnT…' will always evaluate to true\\.$#" + count: 1 + path: ../src/Reflection/Php/NativeBuiltinMethodReflection.php + + - + message: "#^Call to function method_exists\\(\\) with ReflectionProperty and 'isReadOnly' will always evaluate to true\\.$#" + count: 1 + path: ../src/Reflection/Php/PhpPropertyReflection.php + diff --git a/build/composer-require-checker.json b/build/composer-require-checker.json index 45fb391f8d..75cb6d099e 100644 --- a/build/composer-require-checker.json +++ b/build/composer-require-checker.json @@ -3,13 +3,13 @@ "null", "true", "false", "static", "self", "parent", "array", "string", "int", "float", "bool", "iterable", "callable", "void", "object", - "PHPUnit\\Framework\\TestCase", "PHPUnit\\Framework\\AssertionFailedError", "Composer\\Autoload\\ClassLoader", + "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", "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", "Attribute", - "Clue\\React\\Block\\await" + "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", + "Clue\\React\\Block\\await", "Hoa\\File\\Read" ], "php-core-extensions" : [ "Core", diff --git a/build/enum-adapter-errors.neon b/build/enum-adapter-errors.neon new file mode 100644 index 0000000000..7516959bee --- /dev/null +++ b/build/enum-adapter-errors.neon @@ -0,0 +1,42 @@ +parameters: + ignoreErrors: + - + message: "#^Call to method getBackingValue\\(\\) on an unknown class ReflectionEnumBackedCase\\.$#" + count: 2 + path: ../src/Reflection/ClassReflection.php + + - + message: "#^Call to method getCase\\(\\) on an unknown class ReflectionEnum\\.$#" + count: 1 + path: ../src/Reflection/ClassReflection.php + + - + message: "#^Call to method getCases\\(\\) on an unknown class ReflectionEnum\\.$#" + count: 1 + path: ../src/Reflection/ClassReflection.php + + - + message: "#^Call to method isBacked\\(\\) on an unknown class ReflectionEnum\\.$#" + count: 2 + path: ../src/Reflection/ClassReflection.php + + - + message: "#^Call to method getBackingType\\(\\) on an unknown class ReflectionEnum\\.$#" + count: 1 + path: ../src/Reflection/ClassReflection.php + + - + message: "#^Class ReflectionEnum not found\\.$#" + count: 4 + path: ../src/Reflection/ClassReflection.php + + - + message: "#^Class ReflectionEnumBackedCase not found\\.$#" + count: 2 + path: ../src/Reflection/ClassReflection.php + + - + message: "#^Class ReflectionEnum not found\\.$#" + count: 1 + path: ../tests/PHPStan/Reflection/ClassReflectionTest.php + diff --git a/build/enums.neon b/build/enums.neon new file mode 100644 index 0000000000..6c61194dd9 --- /dev/null +++ b/build/enums.neon @@ -0,0 +1,4 @@ +parameters: + excludePaths: + - ../tests/PHPStan/Fixture/TestEnum.php + - ../tests/PHPStan/Fixture/AnotherTestEnum.php diff --git a/build/ignore-by-architecture.neon.php b/build/ignore-by-architecture.neon.php new file mode 100644 index 0000000000..a6c86b46cf --- /dev/null +++ b/build/ignore-by-architecture.neon.php @@ -0,0 +1,11 @@ +load(__DIR__ . '/baseline-32bit.neon'); +} + +return []; diff --git a/build/ignore-by-php-version.neon.php b/build/ignore-by-php-version.neon.php index 44c106ee82..518cc5a40a 100644 --- a/build/ignore-by-php-version.neon.php +++ b/build/ignore-by-php-version.neon.php @@ -1,21 +1,30 @@ load(__DIR__ . '/baseline-lt-7.3.neon')); + $includes[] = __DIR__ . '/baseline-lt-7.3.neon'; } else { - $config = array_merge_recursive($config, $adapter->load(__DIR__ . '/baseline-7.3.neon')); + $includes[] = __DIR__ . '/baseline-7.3.neon'; } if (PHP_VERSION_ID >= 80000) { - $config = array_merge_recursive($config, $adapter->load(__DIR__ . '/baseline-8.0.neon')); + $includes[] = __DIR__ . '/baseline-8.0.neon'; +} +if (PHP_VERSION_ID >= 80100) { + $includes[] = __DIR__ . '/baseline-8.1.neon'; +} else { + $includes[] = __DIR__ . '/enums.neon'; } if (PHP_VERSION_ID >= 70400) { - $config = array_merge_recursive($config, $adapter->load(__DIR__ . '/ignore-gte-php7.4-errors.neon')); + $includes[] = __DIR__ . '/ignore-gte-php7.4-errors.neon'; } +if (PHP_VERSION_ID < 80000) { + $includes[] = __DIR__ . '/enum-adapter-errors.neon'; +} + +$config = []; +$config['includes'] = $includes; +$config['parameters']['phpVersion'] = PHP_VERSION_ID; + return $config; diff --git a/build/ignore-gte-php7.4-errors.neon b/build/ignore-gte-php7.4-errors.neon index cae6484551..54b5cb905d 100644 --- a/build/ignore-gte-php7.4-errors.neon +++ b/build/ignore-gte-php7.4-errors.neon @@ -8,3 +8,6 @@ parameters: path: ../src/Reflection/Php/PhpClassReflectionExtension.php - '#^Class PHPStan\\Rules\\RuleErrors\\RuleError(?:\d+) has an uninitialized property (?:\$message|\$line|\$identifier|\$tip|\$file|\$metadata)#' - '#Extension has an uninitialized property (?:\$typeSpecifier|\$broker)#' + - + message: '#has an uninitialized property#' + path: ../tests diff --git a/build/phpstan.neon b/build/phpstan.neon index 2ae97ea454..aea35d06cc 100644 --- a/build/phpstan.neon +++ b/build/phpstan.neon @@ -8,6 +8,7 @@ includes: - ../conf/bleedingEdge.neon - ../phpstan-baseline.neon - ignore-by-php-version.neon.php + - ignore-by-architecture.neon.php parameters: level: 8 paths: @@ -18,7 +19,7 @@ parameters: - ../tests/phpstan-bootstrap.php checkUninitializedProperties: true checkMissingCallableSignature: true - excludes_analyse: + excludePaths: - ../src/Reflection/SignatureMap/functionMap.php - ../src/Reflection/SignatureMap/functionMetadata.php - ../tests/*/data/* @@ -65,9 +66,6 @@ parameters: check: missingCheckedExceptionInThrows: true tooWideThrowType: true - featureToggles: - readComposerPhpVersion: false - apiRules: false ignoreErrors: - '#^Dynamic call to static method PHPUnit\\Framework\\\S+\(\)\.$#' - '#should be contravariant with parameter \$node \(PhpParser\\Node\) of method PHPStan\\Rules\\Rule::processNode\(\)$#' @@ -91,6 +89,7 @@ parameters: stubFiles: - stubs/ReactChildProcess.stub - stubs/ReactStreams.stub + - stubs/NetteDIContainer.stub services: - class: PHPStan\Build\ServiceLocatorDynamicReturnTypeExtension diff --git a/build/rector-downgrade-vendor.php b/build/rector-downgrade-vendor.php new file mode 100644 index 0000000000..ac77234538 --- /dev/null +++ b/build/rector-downgrade-vendor.php @@ -0,0 +1,28 @@ +parameters(); + $parameters->set(Option::PHP_VERSION_FEATURES, $targetPhpVersionId); + + $services = $containerConfigurator->services(); + + if ($targetPhpVersionId < 70200) { + $services->set(DowngradeObjectTypeDeclarationRector::class); + $services->set(DowngradePregUnmatchedAsNullConstantRector::class); + $services->set(DowngradeStreamIsattyRector::class); + } +}; diff --git a/build/rector-downgrade.php b/build/rector-downgrade.php new file mode 100644 index 0000000000..afdb07beef --- /dev/null +++ b/build/rector-downgrade.php @@ -0,0 +1,63 @@ +parameters(); + + $parameters->set(Option::PHP_VERSION_FEATURES, $targetPhpVersionId); + $parameters->set(Option::SKIP, [ + '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', + ]); + + $services = $containerConfigurator->services(); + + if ($targetPhpVersionId < 80000) { + $services->set(DowngradeTrailingCommasInParamUseRector::class); + $services->set(DowngradeNonCapturingCatchesRector::class); + $services->set(DowngradeUnionTypeTypedPropertyRector::class); + $services->set(DowngradePropertyPromotionRector::class); + $services->set(DowngradeUnionTypeDeclarationRector::class); + } + + if ($targetPhpVersionId < 70400) { + $services->set(DowngradeTypedPropertyRector::class); + $services->set(DowngradeNullCoalescingOperatorRector::class); + $services->set(ArrowFunctionToAnonymousFunctionRector::class); + } + + if ($targetPhpVersionId < 70300) { + $services->set(DowngradeTrailingCommasInFunctionCallsRector::class); + } + + if ($targetPhpVersionId < 70200) { + $services->set(DowngradeObjectTypeDeclarationRector::class); + } +}; diff --git a/build/stubs/NetteDIContainer.stub b/build/stubs/NetteDIContainer.stub new file mode 100644 index 0000000000..455eb43455 --- /dev/null +++ b/build/stubs/NetteDIContainer.stub @@ -0,0 +1,15 @@ + $type + * @return T + */ + public function getByType(string $type); + +} diff --git a/build/transform-source b/build/transform-source new file mode 100755 index 0000000000..71773edfbe --- /dev/null +++ b/build/transform-source @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -o errexit +set -o pipefail +set -o nounset + +export TARGET_PHP_VERSION=$1 + +vendor/bin/rector process src tests/PHPStan tests/e2e -c build/rector-downgrade.php --no-diffs + +vendor/bin/rector process vendor/symfony vendor/nette -c build/rector-downgrade-vendor.php --no-diffs || true diff --git a/compiler/build/box.phar b/compiler/build/box.phar index 76da031be9..d0476cfc4c 100644 Binary files a/compiler/build/box.phar and b/compiler/build/box.phar differ diff --git a/compiler/build/scoper.inc.php b/compiler/build/scoper.inc.php index f2e2ae8151..c2b758dab3 100644 --- a/compiler/build/scoper.inc.php +++ b/compiler/build/scoper.inc.php @@ -16,6 +16,13 @@ '../../stubs', '../../vendor/jetbrains/phpstorm-stubs', '../../vendor/phpstan/php-8-stubs/stubs', + '../../vendor/symfony/polyfill-php80', + '../../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') { continue; @@ -23,8 +30,13 @@ $stubs[] = $file->getPathName(); } +exec('git rev-parse --short HEAD', $gitCommitOutputLines, $gitExitCode); +if ($gitExitCode !== 0) { + die('Could not get Git commit'); +} + return [ - 'prefix' => null, + 'prefix' => sprintf('_PHPStan_%s', $gitCommitOutputLines[0]), 'finders' => [], 'files-whitelist' => $stubs, 'patchers' => [ @@ -75,7 +87,7 @@ function (string $filePath, string $prefix, string $content): string { return $content; }, function (string $filePath, string $prefix, string $content): string { - if ($filePath !== 'src/Testing/TestCase.php') { + if ($filePath !== 'src/Testing/PHPStanTestCase.php') { return $content; } return str_replace(sprintf('\\%s\\PHPUnit\\Framework\\TestCase', $prefix), '\\PHPUnit\\Framework\\TestCase', $content); @@ -155,7 +167,8 @@ function (string $filePath, string $prefix, string $content): string { function (string $filePath, string $prefix, string $content): string { if (!in_array($filePath, [ 'src/Testing/TestCaseSourceLocatorFactory.php', - 'src/Testing/TestCase.php', + 'src/Testing/PHPStanTestCase.php', + 'vendor/ondrejmirtes/better-reflection/src/SourceLocator/Type/ComposerSourceLocator.php', ], true)) { return $content; } @@ -190,18 +203,72 @@ function (string $filePath, string $prefix, string $content): string { return str_replace(sprintf('%s\\ReflectionUnionType', $prefix), 'ReflectionUnionType', $content); }, + function (string $filePath, string $prefix, string $content): string { + if (!in_array($filePath, [ + 'vendor/ondrejmirtes/better-reflection/src/Reflection/Adapter/ReflectionClass.php', + 'vendor/ondrejmirtes/better-reflection/src/Reflection/Adapter/ReflectionClassConstant.php', + 'vendor/ondrejmirtes/better-reflection/src/Reflection/Adapter/ReflectionFunction.php', + 'vendor/ondrejmirtes/better-reflection/src/Reflection/Adapter/ReflectionMethod.php', + 'vendor/ondrejmirtes/better-reflection/src/Reflection/Adapter/ReflectionObject.php', + 'vendor/ondrejmirtes/better-reflection/src/Reflection/Adapter/ReflectionParameter.php', + 'vendor/ondrejmirtes/better-reflection/src/Reflection/Adapter/ReflectionProperty.php', + ], true)) { + return $content; + } + + return str_replace(sprintf('%s\\ReturnTypeWillChange', $prefix), 'ReturnTypeWillChange', $content); + }, + function (string $filePath, string $prefix, string $content): string { + if (!in_array($filePath, [ + 'src/Type/TypehintHelper.php', + 'vendor/ondrejmirtes/better-reflection/src/Reflection/Adapter/ReflectionIntersectionType.php', + 'vendor/ondrejmirtes/better-reflection/src/SourceLocator/SourceStubber/ReflectionSourceStubber.php', + ], true)) { + return $content; + } + + return str_replace(sprintf('%s\\ReflectionIntersectionType', $prefix), 'ReflectionIntersectionType', $content); + }, function (string $filePath, string $prefix, string $content): string { if (strpos($filePath, 'src/') !== 0) { return $content; } return str_replace(sprintf('%s\\Attribute', $prefix), 'Attribute', $content); + }, + function (string $filePath, string $prefix, string $content): string { + if (strpos($filePath, 'src/') !== 0) { + return $content; + } + + return str_replace(sprintf('%s\\ReturnTypeWillChange', $prefix), 'ReturnTypeWillChange', $content); + }, + function (string $filePath, string $prefix, string $content): string { + if ($filePath !== 'vendor/ondrejmirtes/better-reflection/src/SourceLocator/SourceStubber/PhpStormStubsSourceStubber.php') { + return $content; + } + + return str_replace('Core/Core_d.php', 'Core/Core_d.stub', $content); + }, + function (string $filePath, string $prefix, string $content): string { + if ($filePath !== 'vendor/ondrejmirtes/better-reflection/src/SourceLocator/SourceStubber/PhpStormStubsSourceStubber.php') { + return $content; + } + + return str_replace(sprintf('\'%s\\\\JetBrains\\\\', $prefix), '\'JetBrains\\\\', $content); } ], 'whitelist' => [ 'PHPStan\*', 'PhpParser\*', 'Hoa\*', + 'Symfony\Polyfill\Php80\*', + 'Symfony\Polyfill\Mbstring\*', + 'Symfony\Polyfill\Intl\Normalizer\*', + 'Symfony\Polyfill\Php73\*', + 'Symfony\Polyfill\Php74\*', + 'Symfony\Polyfill\Php72\*', + 'Symfony\Polyfill\Intl\Grapheme\*', ], 'whitelist-global-functions' => false, 'whitelist-global-classes' => false, diff --git a/compiler/composer.json b/compiler/composer.json index fb3b8e47c5..89b5e05bf1 100644 --- a/compiler/composer.json +++ b/compiler/composer.json @@ -4,12 +4,12 @@ "description": "PHAR Compiler for PHPStan", "license": ["MIT"], "require": { - "php": "^7.3", + "php": "^8.0", "nette/neon": "^3.0.0", - "symfony/console": "^5.2.2", - "symfony/process": "^5.2.2", - "symfony/filesystem": "^5.2.2", - "symfony/finder": "^5.2.2" + "symfony/console": "^6.0.0", + "symfony/process": "^6.0.0", + "symfony/filesystem": "^6.0.0", + "symfony/finder": "^6.0.0" }, "autoload": { "psr-4": { @@ -23,13 +23,15 @@ }, "require-dev": { "phpunit/phpunit": "^9.5.1", - "phpstan/phpstan-phpunit": "^0.12.8" + "phpstan/phpstan-phpunit": "^1.0" }, "config": { "platform": { - "php": "7.3.24" + "php": "8.0.99" }, "platform-check": false, "sort-packages": true - } + }, + "minimum-stability": "dev", + "prefer-stable": true } diff --git a/compiler/composer.lock b/compiler/composer.lock index 05d5c0ecac..30074d1e1e 100644 --- a/compiler/composer.lock +++ b/compiler/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3d8d846053f4a058c0f88025d0edead6", + "content-hash": "fed40217d551b1c8bdc627086b633e9b", "packages": [ { "name": "nette/neon", - "version": "v3.2.2", + "version": "v3.3.3", "source": { "type": "git", "url": "https://github.com/nette/neon.git", - "reference": "e4ca6f4669121ca6876b1d048c612480e39a28d5" + "reference": "22e384da162fab42961d48eb06c06d3ad0c11b95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/neon/zipball/e4ca6f4669121ca6876b1d048c612480e39a28d5", - "reference": "e4ca6f4669121ca6876b1d048c612480e39a28d5", + "url": "https://api.github.com/repos/nette/neon/zipball/22e384da162fab42961d48eb06c06d3ad0c11b95", + "reference": "22e384da162fab42961d48eb06c06d3ad0c11b95", "shasum": "" }, "require": { @@ -27,12 +27,15 @@ "require-dev": { "nette/tester": "^2.0", "phpstan/phpstan": "^0.12", - "tracy/tracy": "^2.3" + "tracy/tracy": "^2.7" }, + "bin": [ + "bin/neon-lint" + ], "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -67,28 +70,33 @@ ], "support": { "issues": "https://github.com/nette/neon/issues", - "source": "https://github.com/nette/neon/tree/v3.2.2" + "source": "https://github.com/nette/neon/tree/v3.3.3" }, - "time": "2021-02-28T12:30:32+00:00" + "time": "2022-03-10T02:04:26+00:00" }, { "name": "psr/container", - "version": "1.1.1", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", - "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", "shasum": "" }, "require": { - "php": ">=7.2.0" + "php": ">=7.4.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, "autoload": { "psr-4": { "Psr\\Container\\": "src/" @@ -115,51 +123,48 @@ ], "support": { "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/1.1.1" + "source": "https://github.com/php-fig/container/tree/2.0.2" }, - "time": "2021-03-05T17:36:06+00:00" + "time": "2021-11-05T16:47:00+00:00" }, { "name": "symfony/console", - "version": "v5.3.2", + "version": "v6.0.5", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "649730483885ff2ca99ca0560ef0e5f6b03f2ac1" + "reference": "3bebf4108b9e07492a2a4057d207aa5a77d146b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/649730483885ff2ca99ca0560ef0e5f6b03f2ac1", - "reference": "649730483885ff2ca99ca0560ef0e5f6b03f2ac1", + "url": "https://api.github.com/repos/symfony/console/zipball/3bebf4108b9e07492a2a4057d207aa5a77d146b1", + "reference": "3bebf4108b9e07492a2a4057d207aa5a77d146b1", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1", + "php": ">=8.0.2", "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php73": "^1.8", - "symfony/polyfill-php80": "^1.15", - "symfony/service-contracts": "^1.1|^2", - "symfony/string": "^5.1" + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/string": "^5.4|^6.0" }, "conflict": { - "symfony/dependency-injection": "<4.4", - "symfony/dotenv": "<5.1", - "symfony/event-dispatcher": "<4.4", - "symfony/lock": "<4.4", - "symfony/process": "<4.4" + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" }, "provide": { - "psr/log-implementation": "1.0" + "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { - "psr/log": "~1.0", - "symfony/config": "^4.4|^5.0", - "symfony/dependency-injection": "^4.4|^5.0", - "symfony/event-dispatcher": "^4.4|^5.0", - "symfony/lock": "^4.4|^5.0", - "symfony/process": "^4.4|^5.0", - "symfony/var-dumper": "^4.4|^5.0" + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/lock": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0", + "symfony/var-dumper": "^5.4|^6.0" }, "suggest": { "psr/log": "For using the console logger", @@ -199,7 +204,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.3.2" + "source": "https://github.com/symfony/console/tree/v6.0.5" }, "funding": [ { @@ -215,92 +220,26 @@ "type": "tidelift" } ], - "time": "2021-06-12T09:42:48+00:00" - }, - { - "name": "symfony/deprecation-contracts", - "version": "v2.4.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5f38c8804a9e97d23e0c8d63341088cd8a22d627", - "reference": "5f38c8804a9e97d23e0c8d63341088cd8a22d627", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.4-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "files": [ - "function.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.4.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2021-03-23T23:28:01+00:00" + "time": "2022-02-25T10:48:52+00:00" }, { "name": "symfony/filesystem", - "version": "v5.3.0", + "version": "v6.0.6", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "348116319d7fb7d1faa781d26a48922428013eb2" + "reference": "52b888523545b0b4049ab9ce48766802484d7046" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/348116319d7fb7d1faa781d26a48922428013eb2", - "reference": "348116319d7fb7d1faa781d26a48922428013eb2", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/52b888523545b0b4049ab9ce48766802484d7046", + "reference": "52b888523545b0b4049ab9ce48766802484d7046", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-ctype": "~1.8" + "php": ">=8.0.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" }, "type": "library", "autoload": { @@ -328,7 +267,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.3.0" + "source": "https://github.com/symfony/filesystem/tree/v6.0.6" }, "funding": [ { @@ -344,24 +283,24 @@ "type": "tidelift" } ], - "time": "2021-05-26T17:43:10+00:00" + "time": "2022-03-02T12:58:14+00:00" }, { "name": "symfony/finder", - "version": "v5.3.0", + "version": "v6.0.3", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "0ae3f047bed4edff6fd35b26a9a6bfdc92c953c6" + "reference": "8661b74dbabc23223f38c9b99d3f8ade71170430" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/0ae3f047bed4edff6fd35b26a9a6bfdc92c953c6", - "reference": "0ae3f047bed4edff6fd35b26a9a6bfdc92c953c6", + "url": "https://api.github.com/repos/symfony/finder/zipball/8661b74dbabc23223f38c9b99d3f8ade71170430", + "reference": "8661b74dbabc23223f38c9b99d3f8ade71170430", "shasum": "" }, "require": { - "php": ">=7.2.5" + "php": ">=8.0.2" }, "type": "library", "autoload": { @@ -389,7 +328,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.3.0" + "source": "https://github.com/symfony/finder/tree/v6.0.3" }, "funding": [ { @@ -405,25 +344,28 @@ "type": "tidelift" } ], - "time": "2021-05-26T12:52:38+00:00" + "time": "2022-01-26T17:23:29+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.23.0", + "version": "v1.25.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce" + "reference": "30885182c981ab175d4d034db0f6f469898070ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/46cd95797e9df938fdd2b03693b5fca5e64b01ce", - "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", + "reference": "30885182c981ab175d4d034db0f6f469898070ab", "shasum": "" }, "require": { "php": ">=7.1" }, + "provide": { + "ext-ctype": "*" + }, "suggest": { "ext-ctype": "For best performance" }, @@ -438,12 +380,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -468,7 +410,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.23.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0" }, "funding": [ { @@ -484,20 +426,20 @@ "type": "tidelift" } ], - "time": "2021-02-19T12:13:01+00:00" + "time": "2021-10-20T20:35:02+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.23.0", + "version": "v1.25.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "24b72c6baa32c746a4d0840147c9715e42bb68ab" + "reference": "81b86b50cf841a64252b439e738e97f4a34e2783" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/24b72c6baa32c746a4d0840147c9715e42bb68ab", - "reference": "24b72c6baa32c746a4d0840147c9715e42bb68ab", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/81b86b50cf841a64252b439e738e97f4a34e2783", + "reference": "81b86b50cf841a64252b439e738e97f4a34e2783", "shasum": "" }, "require": { @@ -517,12 +459,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -549,7 +491,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.23.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.25.0" }, "funding": [ { @@ -565,11 +507,11 @@ "type": "tidelift" } ], - "time": "2021-05-27T09:17:38+00:00" + "time": "2021-11-23T21:10:46+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.23.0", + "version": "v1.25.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -598,12 +540,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -633,7 +575,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.23.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.25.0" }, "funding": [ { @@ -653,21 +595,24 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.23.0", + "version": "v1.25.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "2df51500adbaebdc4c38dea4c89a2e131c45c8a1" + "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2df51500adbaebdc4c38dea4c89a2e131c45c8a1", - "reference": "2df51500adbaebdc4c38dea4c89a2e131c45c8a1", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825", + "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825", "shasum": "" }, "require": { "php": ">=7.1" }, + "provide": { + "ext-mbstring": "*" + }, "suggest": { "ext-mbstring": "For best performance" }, @@ -682,181 +627,18 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.23.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2021-05-27T09:27:20+00:00" - }, - { - "name": "symfony/polyfill-php73", - "version": "v1.23.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "fba8933c384d6476ab14fb7b8526e5287ca7e010" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fba8933c384d6476ab14fb7b8526e5287ca7e010", - "reference": "fba8933c384d6476ab14fb7b8526e5287ca7e010", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" - }, "files": [ "bootstrap.php" ], - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.23.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2021-02-19T12:13:01+00:00" - }, - { - "name": "symfony/polyfill-php80", - "version": "v1.23.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "eca0bf41ed421bed1b57c4958bab16aa86b757d0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/eca0bf41ed421bed1b57c4958bab16aa86b757d0", - "reference": "eca0bf41ed421bed1b57c4958bab16aa86b757d0", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, - "files": [ - "bootstrap.php" - ], - "classmap": [ - "Resources/stubs" - ] + "Symfony\\Polyfill\\Mbstring\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -866,16 +648,17 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "description": "Symfony polyfill for the Mbstring extension", "homepage": "https://symfony.com", "keywords": [ "compatibility", + "mbstring", "polyfill", "portable", "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.23.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.25.0" }, "funding": [ { @@ -891,25 +674,24 @@ "type": "tidelift" } ], - "time": "2021-02-19T12:13:01+00:00" + "time": "2021-11-30T18:21:41+00:00" }, { "name": "symfony/process", - "version": "v5.3.2", + "version": "v6.0.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "714b47f9196de61a196d86c4bad5f09201b307df" + "reference": "1ccceccc6497e96f4f646218f04b97ae7d9fa7a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/714b47f9196de61a196d86c4bad5f09201b307df", - "reference": "714b47f9196de61a196d86c4bad5f09201b307df", + "url": "https://api.github.com/repos/symfony/process/zipball/1ccceccc6497e96f4f646218f04b97ae7d9fa7a1", + "reference": "1ccceccc6497e96f4f646218f04b97ae7d9fa7a1", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.15" + "php": ">=8.0.2" }, "type": "library", "autoload": { @@ -937,7 +719,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.3.2" + "source": "https://github.com/symfony/process/tree/v6.0.5" }, "funding": [ { @@ -953,25 +735,28 @@ "type": "tidelift" } ], - "time": "2021-06-12T10:15:01+00:00" + "time": "2022-01-30T18:19:12+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.4.0", + "version": "v3.0.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb" + "reference": "36715ebf9fb9db73db0cb24263c79077c6fe8603" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", - "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/36715ebf9fb9db73db0cb24263c79077c6fe8603", + "reference": "36715ebf9fb9db73db0cb24263c79077c6fe8603", "shasum": "" }, "require": { - "php": ">=7.2.5", - "psr/container": "^1.1" + "php": ">=8.0.2", + "psr/container": "^2.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" }, "suggest": { "symfony/service-implementation": "" @@ -979,7 +764,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.4-dev" + "dev-main": "3.0-dev" }, "thanks": { "name": "symfony/contracts", @@ -1016,7 +801,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.4.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.0.0" }, "funding": [ { @@ -1032,44 +817,46 @@ "type": "tidelift" } ], - "time": "2021-04-01T10:43:52+00:00" + "time": "2021-11-04T17:53:12+00:00" }, { "name": "symfony/string", - "version": "v5.3.2", + "version": "v6.0.3", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "0732e97e41c0a590f77e231afc16a327375d50b0" + "reference": "522144f0c4c004c80d56fa47e40e17028e2eefc2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/0732e97e41c0a590f77e231afc16a327375d50b0", - "reference": "0732e97e41c0a590f77e231afc16a327375d50b0", + "url": "https://api.github.com/repos/symfony/string/zipball/522144f0c4c004c80d56fa47e40e17028e2eefc2", + "reference": "522144f0c4c004c80d56fa47e40e17028e2eefc2", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.0.2", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-intl-grapheme": "~1.0", "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "~1.15" + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.0" }, "require-dev": { - "symfony/error-handler": "^4.4|^5.0", - "symfony/http-client": "^4.4|^5.0", - "symfony/translation-contracts": "^1.1|^2", - "symfony/var-exporter": "^4.4|^5.0" + "symfony/error-handler": "^5.4|^6.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/translation-contracts": "^2.0|^3.0", + "symfony/var-exporter": "^5.4|^6.0" }, "type": "library", "autoload": { - "psr-4": { - "Symfony\\Component\\String\\": "" - }, "files": [ "Resources/functions.php" ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, "exclude-from-classmap": [ "/Tests/" ] @@ -1099,7 +886,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.3.2" + "source": "https://github.com/symfony/string/tree/v6.0.3" }, "funding": [ { @@ -1115,35 +902,36 @@ "type": "tidelift" } ], - "time": "2021-06-06T09:51:56+00:00" + "time": "2022-01-02T09:55:41+00:00" } ], "packages-dev": [ { "name": "doctrine/instantiator", - "version": "1.4.0", + "version": "1.4.1", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b" + "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b", - "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc", + "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^8.0", + "doctrine/coding-standard": "^9", "ext-pdo": "*", "ext-phar": "*", - "phpbench/phpbench": "^0.13 || 1.0.0-alpha2", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.22" }, "type": "library", "autoload": { @@ -1170,7 +958,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.4.0" + "source": "https://github.com/doctrine/instantiator/tree/1.4.1" }, "funding": [ { @@ -1186,41 +974,42 @@ "type": "tidelift" } ], - "time": "2020-11-10T18:47:58+00:00" + "time": "2022-03-03T08:28:38+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.10.2", + "version": "1.11.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220" + "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220", - "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", + "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, - "replace": { - "myclabs/deep-copy": "self.version" + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" }, "require-dev": { - "doctrine/collections": "^1.0", - "doctrine/common": "^2.6", - "phpunit/phpunit": "^7.1" + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", "autoload": { - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - }, "files": [ "src/DeepCopy/deep_copy.php" - ] + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1236,7 +1025,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.10.2" + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" }, "funding": [ { @@ -1244,20 +1033,20 @@ "type": "tidelift" } ], - "time": "2020-11-13T09:40:50+00:00" + "time": "2022-03-03T13:19:32+00:00" }, { "name": "nikic/php-parser", - "version": "v4.10.5", + "version": "v4.13.2", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "4432ba399e47c66624bc73c8c0f811e5c109576f" + "reference": "210577fe3cf7badcc5814d99455df46564f3c077" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4432ba399e47c66624bc73c8c0f811e5c109576f", - "reference": "4432ba399e47c66624bc73c8c0f811e5c109576f", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/210577fe3cf7badcc5814d99455df46564f3c077", + "reference": "210577fe3cf7badcc5814d99455df46564f3c077", "shasum": "" }, "require": { @@ -1298,22 +1087,22 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.10.5" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.13.2" }, - "time": "2021-05-03T19:11:20+00:00" + "time": "2021-11-30T19:35:32+00:00" }, { "name": "phar-io/manifest", - "version": "2.0.1", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133" + "reference": "97803eca37d319dfa7826cc2437fc020857acb53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", - "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53", "shasum": "" }, "require": { @@ -1358,22 +1147,22 @@ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/master" + "source": "https://github.com/phar-io/manifest/tree/2.0.3" }, - "time": "2020-06-27T14:33:11+00:00" + "time": "2021-07-20T11:28:43+00:00" }, { "name": "phar-io/version", - "version": "3.1.0", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/phar-io/version.git", - "reference": "bae7c545bef187884426f042434e561ab1ddb182" + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/bae7c545bef187884426f042434e561ab1ddb182", - "reference": "bae7c545bef187884426f042434e561ab1ddb182", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", "shasum": "" }, "require": { @@ -1409,9 +1198,9 @@ "description": "Library for handling version information and constraints", "support": { "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/3.1.0" + "source": "https://github.com/phar-io/version/tree/3.2.1" }, - "time": "2021-02-23T14:00:09+00:00" + "time": "2022-02-21T01:04:05+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -1468,16 +1257,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.2.2", + "version": "5.3.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" + "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", - "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", + "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", "shasum": "" }, "require": { @@ -1488,7 +1277,8 @@ "webmozart/assert": "^1.9.1" }, "require-dev": { - "mockery/mockery": "~1.3.2" + "mockery/mockery": "~1.3.2", + "psalm/phar": "^4.8" }, "type": "library", "extra": { @@ -1518,22 +1308,22 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" }, - "time": "2020-09-03T19:13:55+00:00" + "time": "2021-10-19T17:43:47+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.4.0", + "version": "1.6.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" + "reference": "93ebd0014cab80c4ea9f5e297ea48672f1b87706" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", - "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/93ebd0014cab80c4ea9f5e297ea48672f1b87706", + "reference": "93ebd0014cab80c4ea9f5e297ea48672f1b87706", "shasum": "" }, "require": { @@ -1541,7 +1331,8 @@ "phpdocumentor/reflection-common": "^2.0" }, "require-dev": { - "ext-tokenizer": "*" + "ext-tokenizer": "*", + "psalm/phar": "^4.8" }, "type": "library", "extra": { @@ -1567,39 +1358,39 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.0" }, - "time": "2020-09-17T18:55:26+00:00" + "time": "2022-01-04T19:58:01+00:00" }, { "name": "phpspec/prophecy", - "version": "1.13.0", + "version": "v1.15.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea" + "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/be1996ed8adc35c3fd795488a653f4b518be70ea", - "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/bbcd7380b0ebf3961ee21409db7b38bc31d69a13", + "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13", "shasum": "" }, "require": { "doctrine/instantiator": "^1.2", - "php": "^7.2 || ~8.0, <8.1", + "php": "^7.2 || ~8.0, <8.2", "phpdocumentor/reflection-docblock": "^5.2", "sebastian/comparator": "^3.0 || ^4.0", "sebastian/recursion-context": "^3.0 || ^4.0" }, "require-dev": { - "phpspec/phpspec": "^6.0", + "phpspec/phpspec": "^6.0 || ^7.0", "phpunit/phpunit": "^8.0 || ^9.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.11.x-dev" + "dev-master": "1.x-dev" } }, "autoload": { @@ -1634,22 +1425,22 @@ ], "support": { "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/1.13.0" + "source": "https://github.com/phpspec/prophecy/tree/v1.15.0" }, - "time": "2021-03-17T13:42:18+00:00" + "time": "2021-12-08T12:19:24+00:00" }, { "name": "phpstan/phpstan", - "version": "0.12.90", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "f0e4b56630fc3d4eb5be86606d07212ac212ede4" + "reference": "cbe085f9fdead5b6d62e4c022ca52dc9427a10ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f0e4b56630fc3d4eb5be86606d07212ac212ede4", - "reference": "f0e4b56630fc3d4eb5be86606d07212ac212ede4", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/cbe085f9fdead5b6d62e4c022ca52dc9427a10ee", + "reference": "cbe085f9fdead5b6d62e4c022ca52dc9427a10ee", "shasum": "" }, "require": { @@ -1665,7 +1456,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "0.12-dev" + "dev-master": "1.2-dev" } }, "autoload": { @@ -1680,7 +1471,7 @@ "description": "PHPStan - PHP Static Analysis Tool", "support": { "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/0.12.90" + "source": "https://github.com/phpstan/phpstan/tree/1.2.0" }, "funding": [ { @@ -1700,38 +1491,39 @@ "type": "tidelift" } ], - "time": "2021-06-18T07:15:38+00:00" + "time": "2021-11-18T14:09:01+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "0.12.20", + "version": "1.0.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "efc009981af383eb3303f0ca9868c29acad7ce74" + "reference": "9eb88c9f689003a8a2a5ae9e010338ee94dc39b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/efc009981af383eb3303f0ca9868c29acad7ce74", - "reference": "efc009981af383eb3303f0ca9868c29acad7ce74", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/9eb88c9f689003a8a2a5ae9e010338ee94dc39b3", + "reference": "9eb88c9f689003a8a2a5ae9e010338ee94dc39b3", "shasum": "" }, "require": { "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^0.12.86" + "phpstan/phpstan": "^1.0" }, "conflict": { "phpunit/phpunit": "<7.0" }, "require-dev": { + "nikic/php-parser": "^4.13.0", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-strict-rules": "^0.12.6", + "phpstan/phpstan-strict-rules": "^1.0", "phpunit/phpunit": "^9.5" }, "type": "phpstan-extension", "extra": { "branch-alias": { - "dev-master": "0.12-dev" + "dev-master": "1.0-dev" }, "phpstan": { "includes": [ @@ -1752,29 +1544,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/0.12.20" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.0.0" }, - "time": "2021-06-17T08:28:30+00:00" + "time": "2021-10-14T08:03:54+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.6", + "version": "9.2.15", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "f6293e1b30a2354e8428e004689671b83871edde" + "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f6293e1b30a2354e8428e004689671b83871edde", - "reference": "f6293e1b30a2354e8428e004689671b83871edde", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2e9da11878c4202f97915c1cb4bb1ca318a63f5f", + "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.10.2", + "nikic/php-parser": "^4.13.0", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -1823,7 +1615,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.6" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.15" }, "funding": [ { @@ -1831,20 +1623,20 @@ "type": "github" } ], - "time": "2021-03-28T07:26:59+00:00" + "time": "2022-03-07T09:28:20+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "3.0.5", + "version": "3.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8" + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/aa4be8575f26070b100fccb67faabb28f21f66f8", - "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", "shasum": "" }, "require": { @@ -1883,7 +1675,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.5" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" }, "funding": [ { @@ -1891,7 +1683,7 @@ "type": "github" } ], - "time": "2020-09-28T05:57:25+00:00" + "time": "2021-12-02T12:48:52+00:00" }, { "name": "phpunit/php-invoker", @@ -2076,16 +1868,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.6", + "version": "9.5.18", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "fb9b8333f14e3dce976a60ef6a7e05c7c7ed8bfb" + "reference": "1b5856028273bfd855e60a887278857d872ec67a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fb9b8333f14e3dce976a60ef6a7e05c7c7ed8bfb", - "reference": "fb9b8333f14e3dce976a60ef6a7e05c7c7ed8bfb", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1b5856028273bfd855e60a887278857d872ec67a", + "reference": "1b5856028273bfd855e60a887278857d872ec67a", "shasum": "" }, "require": { @@ -2097,11 +1889,11 @@ "ext-xml": "*", "ext-xmlwriter": "*", "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.1", + "phar-io/manifest": "^2.0.3", "phar-io/version": "^3.0.2", "php": ">=7.3", "phpspec/prophecy": "^1.12.1", - "phpunit/php-code-coverage": "^9.2.3", + "phpunit/php-code-coverage": "^9.2.13", "phpunit/php-file-iterator": "^3.0.5", "phpunit/php-invoker": "^3.1.1", "phpunit/php-text-template": "^2.0.3", @@ -2136,11 +1928,11 @@ } }, "autoload": { - "classmap": [ - "src/" - ], "files": [ "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -2163,11 +1955,11 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.6" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.18" }, "funding": [ { - "url": "https://phpunit.de/donate.html", + "url": "https://phpunit.de/sponsors.html", "type": "custom" }, { @@ -2175,7 +1967,7 @@ "type": "github" } ], - "time": "2021-06-23T05:14:38+00:00" + "time": "2022-03-08T06:52:28+00:00" }, { "name": "sebastian/cli-parser", @@ -2606,16 +2398,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.3", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65" + "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/d89cc98761b8cb5a1a235a6b703ae50d34080e65", - "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/65e8b7db476c5dd267e65eea9cab77584d3cfff9", + "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9", "shasum": "" }, "require": { @@ -2664,14 +2456,14 @@ } ], "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "http://www.github.com/sebastianbergmann/exporter", + "homepage": "https://www.github.com/sebastianbergmann/exporter", "keywords": [ "export", "exporter" ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.3" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4" }, "funding": [ { @@ -2679,20 +2471,20 @@ "type": "github" } ], - "time": "2020-09-28T05:24:23+00:00" + "time": "2021-11-11T14:18:36+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.3", + "version": "5.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "23bd5951f7ff26f12d4e3242864df3e08dec4e49" + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/23bd5951f7ff26f12d4e3242864df3e08dec4e49", - "reference": "23bd5951f7ff26f12d4e3242864df3e08dec4e49", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", + "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", "shasum": "" }, "require": { @@ -2735,7 +2527,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.3" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" }, "funding": [ { @@ -2743,7 +2535,7 @@ "type": "github" } ], - "time": "2021-06-11T13:31:12+00:00" + "time": "2022-02-14T08:28:10+00:00" }, { "name": "sebastian/lines-of-code", @@ -3143,16 +2935,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.0", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "75a63c33a8577608444246075ea0af0d052e452a" + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a", - "reference": "75a63c33a8577608444246075ea0af0d052e452a", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", "shasum": "" }, "require": { @@ -3181,7 +2973,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/master" + "source": "https://github.com/theseer/tokenizer/tree/1.2.1" }, "funding": [ { @@ -3189,7 +2981,7 @@ "type": "github" } ], - "time": "2020-07-12T23:59:07+00:00" + "time": "2021-07-28T10:34:58+00:00" }, { "name": "webmozart/assert", @@ -3251,16 +3043,16 @@ } ], "aliases": [], - "minimum-stability": "stable", + "minimum-stability": "dev", "stability-flags": [], - "prefer-stable": false, + "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^7.3" + "php": "^8.0" }, "platform-dev": [], "platform-overrides": { - "php": "7.3.24" + "php": "8.0.99" }, - "plugin-api-version": "2.1.0" + "plugin-api-version": "2.2.0" } diff --git a/compiler/patches/stubs/PDO/PDO.stub.patch b/compiler/patches/stubs/PDO/PDO.stub.patch deleted file mode 100644 index 0f65cc6010..0000000000 --- a/compiler/patches/stubs/PDO/PDO.stub.patch +++ /dev/null @@ -1,18 +0,0 @@ ---- PDO/PDO.stub 2020-06-14 14:26:12.000000000 +0200 -+++ PDO/PDO2.stub 2020-06-14 14:26:12.000000000 +0200 -@@ -843,6 +843,15 @@ - */ - const SQLITE_ATTR_EXTENDED_RESULT_CODES = 2; - -+ const FB_ATTR_DATE_FORMAT = 1; -+ const FB_ATTR_TIME_FORMAT = 2; -+ const FB_ATTR_TIMESTAMP_FORMAT = 3; -+ -+ const OCI_ATTR_ACTION = 1; -+ const OCI_ATTR_CLIENT_INFO = 2; -+ const OCI_ATTR_CLIENT_IDENTIFIER = 3; -+ const OCI_ATTR_MODULE = 4; -+ - /** - * (PHP 5 >= 5.1.0, PHP 7, PECL pdo >= 0.1.0)
- * Creates a PDO instance representing a connection to a database diff --git a/compiler/src/Console/CompileCommand.php b/compiler/src/Console/CompileCommand.php index 194198f2f2..16834d69e2 100644 --- a/compiler/src/Console/CompileCommand.php +++ b/compiler/src/Console/CompileCommand.php @@ -2,40 +2,46 @@ namespace PHPStan\Compiler\Console; +use Exception; use PHPStan\Compiler\Filesystem\Filesystem; use PHPStan\Compiler\Process\ProcessFactory; +use PHPStan\ShouldNotHappenException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Finder\Finder; +use function chdir; +use function dirname; use function escapeshellarg; +use function exec; +use function file_get_contents; +use function file_put_contents; +use function implode; +use function is_dir; +use function json_decode; +use function json_encode; +use function realpath; +use function rename; +use function sprintf; +use function str_replace; +use function strlen; +use function substr; +use function unlink; +use function var_export; +use const JSON_PRETTY_PRINT; +use const JSON_UNESCAPED_SLASHES; final class CompileCommand extends Command { - /** @var Filesystem */ - private $filesystem; - - /** @var ProcessFactory */ - private $processFactory; - - /** @var string */ - private $dataDir; - - /** @var string */ - private $buildDir; - public function __construct( - Filesystem $filesystem, - ProcessFactory $processFactory, - string $dataDir, - string $buildDir + private Filesystem $filesystem, + private ProcessFactory $processFactory, + private string $dataDir, + private string $buildDir, ) { parent::__construct(); - $this->filesystem = $filesystem; - $this->processFactory = $processFactory; - $this->dataDir = $dataDir; - $this->buildDir = $buildDir; } protected function configure(): void @@ -52,7 +58,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->deleteUnnecessaryVendorCode(); $this->fixComposerJson($this->buildDir); $this->renamePhpStormStubs(); - $this->patchPhpStormStubs($output); $this->renamePhp8Stubs(); $this->transformSource(); @@ -74,7 +79,7 @@ private function fixComposerJson(string $buildDir): void $encodedJson = json_encode($json, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); if ($encodedJson === false) { - throw new \Exception('json_encode() was not successful.'); + throw new Exception('json_encode() was not successful.'); } $this->filesystem->write($buildDir . '/composer.json', $encodedJson); @@ -87,8 +92,11 @@ private function renamePhpStormStubs(): void return; } - $stubFinder = \Symfony\Component\Finder\Finder::create(); - $stubsMapPath = $directory . '/PhpStormStubsMap.php'; + $stubFinder = Finder::create(); + $stubsMapPath = realpath($directory . '/PhpStormStubsMap.php'); + if ($stubsMapPath === false) { + throw new Exception('realpath() failed'); + } foreach ($stubFinder->files()->name('*.php')->in($directory) as $stubFile) { $path = $stubFile->getPathname(); if ($path === $stubsMapPath) { @@ -97,23 +105,23 @@ private function renamePhpStormStubs(): void $renameSuccess = rename( $path, - dirname($path) . '/' . $stubFile->getBasename('.php') . '.stub' + dirname($path) . '/' . $stubFile->getBasename('.php') . '.stub', ); if ($renameSuccess === false) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Could not rename %s', $path)); + throw new ShouldNotHappenException(sprintf('Could not rename %s', $path)); } } $stubsMapContents = file_get_contents($stubsMapPath); if ($stubsMapContents === false) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Could not read %s', $stubsMapPath)); + throw new ShouldNotHappenException(sprintf('Could not read %s', $stubsMapPath)); } $stubsMapContents = str_replace('.php\',', '.stub\',', $stubsMapContents); $putSuccess = file_put_contents($stubsMapPath, $stubsMapContents); if ($putSuccess === false) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Could not write %s', $stubsMapPath)); + throw new ShouldNotHappenException(sprintf('Could not write %s', $stubsMapPath)); } } @@ -124,7 +132,7 @@ private function renamePhp8Stubs(): void return; } - $stubFinder = \Symfony\Component\Finder\Finder::create(); + $stubFinder = Finder::create(); $stubsMapPath = $directory . '/../Php8StubsMap.php'; foreach ($stubFinder->files()->name('*.php')->in($directory) as $stubFile) { $path = $stubFile->getPathname(); @@ -134,39 +142,23 @@ private function renamePhp8Stubs(): void $renameSuccess = rename( $path, - dirname($path) . '/' . $stubFile->getBasename('.php') . '.stub' + dirname($path) . '/' . $stubFile->getBasename('.php') . '.stub', ); if ($renameSuccess === false) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Could not rename %s', $path)); + throw new ShouldNotHappenException(sprintf('Could not rename %s', $path)); } } $stubsMapContents = file_get_contents($stubsMapPath); if ($stubsMapContents === false) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Could not read %s', $stubsMapPath)); + throw new ShouldNotHappenException(sprintf('Could not read %s', $stubsMapPath)); } $stubsMapContents = str_replace('.php\',', '.stub\',', $stubsMapContents); $putSuccess = file_put_contents($stubsMapPath, $stubsMapContents); if ($putSuccess === false) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Could not write %s', $stubsMapPath)); - } - } - - private function patchPhpStormStubs(OutputInterface $output): void - { - $stubFinder = \Symfony\Component\Finder\Finder::create(); - $stubsDirectory = __DIR__ . '/../../../vendor/jetbrains/phpstorm-stubs'; - foreach ($stubFinder->files()->name('*.patch')->in(__DIR__ . '/../../patches/stubs') as $patchFile) { - $absolutePatchPath = $patchFile->getPathname(); - $patchPath = $patchFile->getRelativePathname(); - $stubPath = realpath($stubsDirectory . '/' . dirname($patchPath) . '/' . basename($patchPath, '.patch')); - if ($stubPath === false) { - $output->writeln(sprintf('Stub %s not found.', $stubPath)); - continue; - } - $this->patchFile($output, $stubPath, $absolutePatchPath); + throw new ShouldNotHappenException(sprintf('Could not write %s', $stubsMapPath)); } } @@ -183,7 +175,7 @@ private function buildPreloadScript(): void %s php; - $finder = \Symfony\Component\Finder\Finder::create(); + $finder = Finder::create(); $root = realpath(__DIR__ . '/../../..'); if ($root === false) { return; @@ -218,29 +210,15 @@ private function deleteUnnecessaryVendorCode(): void @unlink($vendorDir . '/nikic/php-parser/bin/php-parse'); } - private function patchFile(OutputInterface $output, string $originalFile, string $patchFile): void - { - exec(sprintf( - 'patch -d %s %s %s', - escapeshellarg($this->buildDir), - escapeshellarg($originalFile), - escapeshellarg($patchFile) - ), $outputLines, $exitCode); - if ($exitCode === 0) { - return; - } - - $output->writeln(sprintf('Patching failed: %s', implode("\n", $outputLines))); - } - private function transformSource(): void { - exec(escapeshellarg(__DIR__ . '/../../../bin/transform-source.php'), $outputLines, $exitCode); + chdir(__DIR__ . '/../../..'); + exec(escapeshellarg(__DIR__ . '/../../../build/transform-source') . ' 7.1', $outputLines, $exitCode); if ($exitCode === 0) { return; } - throw new \PHPStan\ShouldNotHappenException(implode("\n", $outputLines)); + throw new ShouldNotHappenException(implode("\n", $outputLines)); } } diff --git a/compiler/src/Filesystem/SymfonyFilesystem.php b/compiler/src/Filesystem/SymfonyFilesystem.php index 7946e92626..e74d01fd74 100644 --- a/compiler/src/Filesystem/SymfonyFilesystem.php +++ b/compiler/src/Filesystem/SymfonyFilesystem.php @@ -2,15 +2,15 @@ namespace PHPStan\Compiler\Filesystem; +use RuntimeException; +use function file_get_contents; +use function file_put_contents; + final class SymfonyFilesystem implements Filesystem { - /** @var \Symfony\Component\Filesystem\Filesystem */ - private $filesystem; - - public function __construct(\Symfony\Component\Filesystem\Filesystem $filesystem) + public function __construct(private \Symfony\Component\Filesystem\Filesystem $filesystem) { - $this->filesystem = $filesystem; } public function exists(string $dir): bool @@ -32,7 +32,7 @@ public function read(string $file): string { $content = file_get_contents($file); if ($content === false) { - throw new \RuntimeException(); + throw new RuntimeException(); } return $content; } diff --git a/compiler/src/Process/DefaultProcessFactory.php b/compiler/src/Process/DefaultProcessFactory.php index 3f4a09a6b1..3df3ce50da 100644 --- a/compiler/src/Process/DefaultProcessFactory.php +++ b/compiler/src/Process/DefaultProcessFactory.php @@ -8,8 +8,7 @@ final class DefaultProcessFactory implements ProcessFactory { - /** @var OutputInterface */ - private $output; + private OutputInterface $output; public function __construct() { @@ -18,8 +17,6 @@ public function __construct() /** * @param string[] $command - * @param string $cwd - * @return \PHPStan\Compiler\Process\Process */ public function create(array $command, string $cwd): Process { diff --git a/compiler/src/Process/ProcessFactory.php b/compiler/src/Process/ProcessFactory.php index f892601f73..b2225f5343 100644 --- a/compiler/src/Process/ProcessFactory.php +++ b/compiler/src/Process/ProcessFactory.php @@ -9,8 +9,6 @@ interface ProcessFactory /** * @param string[] $command - * @param string $cwd - * @return \PHPStan\Compiler\Process\Process */ public function create(array $command, string $cwd): Process; diff --git a/compiler/src/Process/SymfonyProcess.php b/compiler/src/Process/SymfonyProcess.php index 56e8b1f5ec..8fbfea879e 100644 --- a/compiler/src/Process/SymfonyProcess.php +++ b/compiler/src/Process/SymfonyProcess.php @@ -8,12 +8,10 @@ final class SymfonyProcess implements Process { /** @var \Symfony\Component\Process\Process */ - private $process; + private \Symfony\Component\Process\Process $process; /** * @param string[] $command - * @param string $cwd - * @param \Symfony\Component\Console\Output\OutputInterface $output */ public function __construct(array $command, string $cwd, OutputInterface $output) { diff --git a/compiler/tests/Console/CompileCommandTest.php b/compiler/tests/Console/CompileCommandTest.php index 72e1a5d5dc..000b738533 100644 --- a/compiler/tests/Console/CompileCommandTest.php +++ b/compiler/tests/Console/CompileCommandTest.php @@ -30,8 +30,7 @@ public function testCommand(): void } } } -EOT - ); +EOT); $process = $this->createMock(Process::class); diff --git a/compiler/tests/Filesystem/SymfonyFilesystemTest.php b/compiler/tests/Filesystem/SymfonyFilesystemTest.php index 0d21aff557..5a0f486441 100644 --- a/compiler/tests/Filesystem/SymfonyFilesystemTest.php +++ b/compiler/tests/Filesystem/SymfonyFilesystemTest.php @@ -3,13 +3,15 @@ namespace PHPStan\Compiler\Filesystem; use PHPUnit\Framework\TestCase; +use Symfony\Component\Filesystem\Filesystem; +use function unlink; final class SymfonyFilesystemTest extends TestCase { public function testExists(): void { - $inner = $this->createMock(\Symfony\Component\Filesystem\Filesystem::class); + $inner = $this->createMock(Filesystem::class); $inner->expects(self::once())->method('exists')->with('foo')->willReturn(true); self::assertTrue((new SymfonyFilesystem($inner))->exists('foo')); @@ -17,7 +19,7 @@ public function testExists(): void public function testRemove(): void { - $inner = $this->createMock(\Symfony\Component\Filesystem\Filesystem::class); + $inner = $this->createMock(Filesystem::class); $inner->expects(self::once())->method('remove')->with('foo')->willReturn(true); (new SymfonyFilesystem($inner))->remove('foo'); @@ -25,7 +27,7 @@ public function testRemove(): void public function testMkdir(): void { - $inner = $this->createMock(\Symfony\Component\Filesystem\Filesystem::class); + $inner = $this->createMock(Filesystem::class); $inner->expects(self::once())->method('mkdir')->with('foo')->willReturn(true); (new SymfonyFilesystem($inner))->mkdir('foo'); @@ -33,7 +35,7 @@ public function testMkdir(): void public function testRead(): void { - $inner = $this->createMock(\Symfony\Component\Filesystem\Filesystem::class); + $inner = $this->createMock(Filesystem::class); $content = (new SymfonyFilesystem($inner))->read(__DIR__ . '/data/composer.json'); self::assertSame("{}\n", $content); @@ -41,7 +43,7 @@ public function testRead(): void public function testWrite(): void { - $inner = $this->createMock(\Symfony\Component\Filesystem\Filesystem::class); + $inner = $this->createMock(Filesystem::class); @unlink(__DIR__ . '/data/test.json'); (new SymfonyFilesystem($inner))->write(__DIR__ . '/data/test.json', "{}\n"); diff --git a/composer.json b/composer.json index 22f869022e..45c70f5f28 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "MIT" ], "require": { - "php": "^7.4 || ^8.0", + "php": "^8.0", "clue/block-react": "^1.4", "clue/ndjson-react": "^1.0", "composer/ca-bundle": "^1.2", @@ -14,27 +14,36 @@ "hoa/exception": "^1.0", "hoa/regex": "1.17.01.13", "jean85/pretty-package-versions": "^1.0.3", - "jetbrains/phpstorm-stubs": "dev-master#2000119caf61d226e7facad68d7a260d8616a925", + "jetbrains/phpstorm-stubs": "dev-master#25c2ba4791d7c08e62aae0793336694ea4fd424a", "nette/bootstrap": "^3.0", - "nette/di": "^3.0.5", + "nette/di": "^3.0.11", "nette/finder": "^2.5", - "nette/neon": "^3.0", - "nette/schema": "^1.0", - "nette/utils": "^3.1.3", - "nikic/php-parser": "4.10.5", + "nette/neon": "^3.3.1", + "nette/schema": "^1.2.2", + "nette/utils": "^3.2.5", + "nikic/php-parser": "^4.13.2", "ondram/ci-detector": "^3.4.0", - "ondrejmirtes/better-reflection": "4.3.60", - "phpstan/php-8-stubs": "^0.1.21", - "phpstan/phpdoc-parser": "^0.5.5", - "react/child-process": "^0.6.1", - "react/event-loop": "^1.1", + "ondrejmirtes/better-reflection": "5.0.7.2", + "phpstan/php-8-stubs": "0.1.49", + "phpstan/phpdoc-parser": "^1.2.0", + "react/child-process": "^0.6.4", + "react/event-loop": "^1.2", "react/http": "^1.1", "react/promise": "^2.8", "react/socket": "^1.3", "react/stream": "^1.1", - "symfony/console": "^4.3", - "symfony/finder": "^4.3", - "symfony/service-contracts": "1.1.8" + "symfony/console": "^5.4.3", + "symfony/finder": "^5.4.3", + "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/process": "^5.4.3", + "symfony/service-contracts": "^2.5.0", + "symfony/string": "^5.4.3" }, "replace": { "phpstan/phpstan": "self.version" @@ -43,25 +52,27 @@ "brianium/paratest": "^6.2.0", "nategood/httpful": "^0.2.20", "php-parallel-lint/php-parallel-lint": "^1.2.0", - "phpstan/phpstan-deprecation-rules": "^0.12.3", - "phpstan/phpstan-nette": "^0.12.18", - "phpstan/phpstan-php-parser": "^0.12", - "phpstan/phpstan-phpunit": "^0.12.19", - "phpstan/phpstan-strict-rules": "^0.12", + "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.0", "phpunit/phpunit": "^9.5.4", + "rector/rector": "^0.12.15", "vaimo/composer-patches": "^4.22" }, "config": { "platform": { - "php": "7.4.6" + "php": "8.0.99" }, "platform-check": false, - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "composer/package-versions-deprecated": true, + "vaimo/composer-patches": true + } }, "extra": { - "branch-alias": { - "dev-master": "0.12-dev" - }, "patcher": { "search": "patches" } diff --git a/composer.lock b/composer.lock index d6f0ca8b96..1f89e62581 100644 --- a/composer.lock +++ b/composer.lock @@ -4,31 +4,31 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "232996096032e099e1ea42f579ec050d", + "content-hash": "317daa3b733dc326442f1c6bd70a8333", "packages": [ { "name": "clue/block-react", - "version": "v1.4.0", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/clue/reactphp-block.git", - "reference": "c8e7583ae55127b89d6915480ce295bac81c4f88" + "reference": "718b0571a94aa693c6fffc72182e87257ac900f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/clue/reactphp-block/zipball/c8e7583ae55127b89d6915480ce295bac81c4f88", - "reference": "c8e7583ae55127b89d6915480ce295bac81c4f88", + "url": "https://api.github.com/repos/clue/reactphp-block/zipball/718b0571a94aa693c6fffc72182e87257ac900f3", + "reference": "718b0571a94aa693c6fffc72182e87257ac900f3", "shasum": "" }, "require": { "php": ">=5.3", - "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", - "react/promise": "^2.7 || ^1.2.1", + "react/event-loop": "^1.2", + "react/promise": "^3.0 || ^2.7 || ^1.2.1", "react/promise-timer": "^1.5" }, "require-dev": { "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", - "react/http": "^1.0" + "react/http": "^1.4" }, "type": "library", "autoload": { @@ -60,7 +60,7 @@ ], "support": { "issues": "https://github.com/clue/reactphp-block/issues", - "source": "https://github.com/clue/reactphp-block/tree/v1.4.0" + "source": "https://github.com/clue/reactphp-block/tree/v1.5.0" }, "funding": [ { @@ -72,20 +72,20 @@ "type": "github" } ], - "time": "2020-08-21T14:09:44+00:00" + "time": "2021-10-20T14:07:33+00:00" }, { "name": "clue/ndjson-react", - "version": "v1.1.0", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/clue/reactphp-ndjson.git", - "reference": "767ec9543945802b5766fab0da4520bf20626f66" + "reference": "708411c7e45ac85371a99d50f52284971494bede" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/767ec9543945802b5766fab0da4520bf20626f66", - "reference": "767ec9543945802b5766fab0da4520bf20626f66", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/708411c7e45ac85371a99d50f52284971494bede", + "reference": "708411c7e45ac85371a99d50f52284971494bede", "shasum": "" }, "require": { @@ -93,7 +93,7 @@ "react/stream": "^1.0 || ^0.7 || ^0.6" }, "require-dev": { - "phpunit/phpunit": "^7.0 || ^6.0 || ^5.7 || ^4.8.35", + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3" }, "type": "library", @@ -124,22 +124,32 @@ ], "support": { "issues": "https://github.com/clue/reactphp-ndjson/issues", - "source": "https://github.com/clue/reactphp-ndjson/tree/v1.1.0" + "source": "https://github.com/clue/reactphp-ndjson/tree/v1.2.0" }, - "time": "2020-02-04T11:48:52+00:00" + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2020-12-09T13:09:07+00:00" }, { "name": "composer/ca-bundle", - "version": "1.2.8", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "8a7ecad675253e4654ea05505233285377405215" + "reference": "4c679186f2aca4ab6a0f1b0b9cf9252decb44d0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/8a7ecad675253e4654ea05505233285377405215", - "reference": "8a7ecad675253e4654ea05505233285377405215", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/4c679186f2aca4ab6a0f1b0b9cf9252decb44d0b", + "reference": "4c679186f2aca4ab6a0f1b0b9cf9252decb44d0b", "shasum": "" }, "require": { @@ -148,14 +158,15 @@ "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8", + "phpstan/phpstan": "^0.12.55", "psr/log": "^1.0", - "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0" + "symfony/phpunit-bridge": "^4.2 || ^5", + "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-main": "1.x-dev" } }, "autoload": { @@ -185,7 +196,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.2.8" + "source": "https://github.com/composer/ca-bundle/tree/1.3.1" }, "funding": [ { @@ -201,20 +212,20 @@ "type": "tidelift" } ], - "time": "2020-08-23T12:54:47+00:00" + "time": "2021-10-28T20:44:15+00:00" }, { "name": "composer/package-versions-deprecated", - "version": "1.11.99", + "version": "1.11.99.4", "source": { "type": "git", "url": "https://github.com/composer/package-versions-deprecated.git", - "reference": "c8c9aa8a14cc3d3bec86d0a8c3fa52ea79936855" + "reference": "b174585d1fe49ceed21928a945138948cb394600" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/c8c9aa8a14cc3d3bec86d0a8c3fa52ea79936855", - "reference": "c8c9aa8a14cc3d3bec86d0a8c3fa52ea79936855", + "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/b174585d1fe49ceed21928a945138948cb394600", + "reference": "b174585d1fe49ceed21928a945138948cb394600", "shasum": "" }, "require": { @@ -258,7 +269,78 @@ "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", "support": { "issues": "https://github.com/composer/package-versions-deprecated/issues", - "source": "https://github.com/composer/package-versions-deprecated/tree/master" + "source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2021-09-13T08:41:34+00:00" + }, + { + "name": "composer/pcre", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "3d322d715c43a1ac36c7fe215fa59336265500f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/3d322d715c43a1ac36c7fe215fa59336265500f2", + "reference": "3d322d715c43a1ac36c7fe215fa59336265500f2", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/1.0.0" }, "funding": [ { @@ -274,29 +356,31 @@ "type": "tidelift" } ], - "time": "2020-08-25T05:50:16+00:00" + "time": "2021-12-06T15:17:27+00:00" }, { "name": "composer/xdebug-handler", - "version": "2.0.1", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "964adcdd3a28bf9ed5d9ac6450064e0d71ed7496" + "reference": "6555461e76962fd0379c444c46fd558a0fcfb65e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/964adcdd3a28bf9ed5d9ac6450064e0d71ed7496", - "reference": "964adcdd3a28bf9ed5d9ac6450064e0d71ed7496", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6555461e76962fd0379c444c46fd558a0fcfb65e", + "reference": "6555461e76962fd0379c444c46fd558a0fcfb65e", "shasum": "" }, "require": { + "composer/pcre": "^1", "php": "^5.3.2 || ^7.0 || ^8.0", - "psr/log": "^1.0" + "psr/log": "^1 || ^2 || ^3" }, "require-dev": { - "phpstan/phpstan": "^0.12.55", - "symfony/phpunit-bridge": "^4.2 || ^5" + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^4.2 || ^5.0 || ^6.0" }, "type": "library", "autoload": { @@ -322,7 +406,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/2.0.1" + "source": "https://github.com/composer/xdebug-handler/tree/2.0.3" }, "funding": [ { @@ -338,7 +422,7 @@ "type": "tidelift" } ], - "time": "2021-05-05T19:37:51+00:00" + "time": "2021-12-08T13:07:32+00:00" }, { "name": "evenement/evenement", @@ -475,6 +559,7 @@ "issues": "https://github.com/hoaproject/Compiler/issues", "source": "https://central.hoa-project.net/Resource/Library/Compiler" }, + "abandoned": true, "time": "2017-08-08T07:44:07+00:00" }, { @@ -546,6 +631,7 @@ "issues": "https://github.com/hoaproject/Consistency/issues", "source": "https://central.hoa-project.net/Resource/Library/Consistency" }, + "abandoned": true, "time": "2017-05-02T12:18:12+00:00" }, { @@ -610,6 +696,7 @@ "issues": "https://github.com/hoaproject/Event/issues", "source": "https://central.hoa-project.net/Resource/Library/Event" }, + "abandoned": true, "time": "2017-01-13T15:30:50+00:00" }, { @@ -672,6 +759,7 @@ "issues": "https://github.com/hoaproject/Exception/issues", "source": "https://central.hoa-project.net/Resource/Library/Exception" }, + "abandoned": true, "time": "2017-01-16T07:53:27+00:00" }, { @@ -742,6 +830,7 @@ "issues": "https://github.com/hoaproject/File/issues", "source": "https://central.hoa-project.net/Resource/Library/File" }, + "abandoned": true, "time": "2017-07-11T07:42:15+00:00" }, { @@ -804,6 +893,7 @@ "issues": "https://github.com/hoaproject/Iterator/issues", "source": "https://central.hoa-project.net/Resource/Library/Iterator" }, + "abandoned": true, "time": "2017-01-10T10:34:47+00:00" }, { @@ -877,6 +967,7 @@ "issues": "https://github.com/hoaproject/Math/issues", "source": "https://central.hoa-project.net/Resource/Library/Math" }, + "abandoned": true, "time": "2017-05-16T08:02:17+00:00" }, { @@ -945,6 +1036,7 @@ "issues": "https://github.com/hoaproject/Protocol/issues", "source": "https://central.hoa-project.net/Resource/Library/Protocol" }, + "abandoned": true, "time": "2017-01-14T12:26:10+00:00" }, { @@ -1009,6 +1101,7 @@ "issues": "https://github.com/hoaproject/Regex/issues", "source": "https://central.hoa-project.net/Resource/Library/Regex" }, + "abandoned": true, "time": "2017-01-13T16:10:24+00:00" }, { @@ -1081,6 +1174,7 @@ "issues": "https://github.com/hoaproject/Stream/issues", "source": "https://central.hoa-project.net/Resource/Library/Stream" }, + "abandoned": true, "time": "2017-02-21T16:01:06+00:00" }, { @@ -1149,6 +1243,7 @@ "issues": "https://github.com/hoaproject/Ustring/issues", "source": "https://central.hoa-project.net/Resource/Library/Ustring" }, + "abandoned": true, "time": "2017-01-16T07:08:25+00:00" }, { @@ -1212,6 +1307,7 @@ "issues": "https://github.com/hoaproject/Visitor/issues", "source": "https://central.hoa-project.net/Resource/Library/Visitor" }, + "abandoned": true, "time": "2017-01-16T07:02:03+00:00" }, { @@ -1272,6 +1368,7 @@ "issues": "https://github.com/hoaproject/Zformat/issues", "source": "https://central.hoa-project.net/Resource/Library/Zformat" }, + "abandoned": true, "time": "2017-01-10T10:39:54+00:00" }, { @@ -1335,12 +1432,12 @@ "source": { "type": "git", "url": "https://github.com/JetBrains/phpstorm-stubs.git", - "reference": "2000119caf61d226e7facad68d7a260d8616a925" + "reference": "25c2ba4791d7c08e62aae0793336694ea4fd424a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/2000119caf61d226e7facad68d7a260d8616a925", - "reference": "2000119caf61d226e7facad68d7a260d8616a925", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/25c2ba4791d7c08e62aae0793336694ea4fd424a", + "reference": "25c2ba4791d7c08e62aae0793336694ea4fd424a", "shasum": "" }, "require-dev": { @@ -1376,33 +1473,33 @@ "support": { "source": "https://github.com/JetBrains/phpstorm-stubs/tree/master" }, - "time": "2021-05-18T14:05:15+00:00" + "time": "2022-03-15T14:13:43+00:00" }, { "name": "nette/bootstrap", - "version": "v3.0.2", + "version": "v3.1.2", "source": { "type": "git", "url": "https://github.com/nette/bootstrap.git", - "reference": "67830a65b42abfb906f8e371512d336ebfb5da93" + "reference": "3ab4912a08af0c16d541c3709935c3478b5ee090" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/bootstrap/zipball/67830a65b42abfb906f8e371512d336ebfb5da93", - "reference": "67830a65b42abfb906f8e371512d336ebfb5da93", + "url": "https://api.github.com/repos/nette/bootstrap/zipball/3ab4912a08af0c16d541c3709935c3478b5ee090", + "reference": "3ab4912a08af0c16d541c3709935c3478b5ee090", "shasum": "" }, "require": { - "nette/di": "^3.0", - "nette/utils": "^3.0", - "php": ">=7.1" + "nette/di": "^3.0.5", + "nette/utils": "^3.2.1", + "php": ">=7.2 <8.2" }, "conflict": { "tracy/tracy": "<2.6" }, "require-dev": { - "latte/latte": "^2.2", - "nette/application": "^3.0", + "latte/latte": "^2.8", + "nette/application": "^3.1", "nette/caching": "^3.0", "nette/database": "^3.0", "nette/forms": "^3.0", @@ -1422,7 +1519,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -1446,7 +1543,7 @@ "homepage": "https://nette.org/contributors" } ], - "description": "🅱 Nette Bootstrap: the simple way to configure and bootstrap your Nette application.", + "description": "🅱 Nette Bootstrap: the simple way to configure and bootstrap your Nette application.", "homepage": "https://nette.org", "keywords": [ "bootstrapping", @@ -1455,32 +1552,32 @@ ], "support": { "issues": "https://github.com/nette/bootstrap/issues", - "source": "https://github.com/nette/bootstrap/tree/master" + "source": "https://github.com/nette/bootstrap/tree/v3.1.2" }, - "time": "2020-05-26T08:46:23+00:00" + "time": "2021-11-24T16:51:46+00:00" }, { "name": "nette/di", - "version": "v3.0.5", + "version": "v3.0.11", "source": { "type": "git", "url": "https://github.com/nette/di.git", - "reference": "766e8185196a97ded4f9128db6d79a3a124b7eb6" + "reference": "942e406f63b88b57cb4e095ae0fd95c103d12c5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/di/zipball/766e8185196a97ded4f9128db6d79a3a124b7eb6", - "reference": "766e8185196a97ded4f9128db6d79a3a124b7eb6", + "url": "https://api.github.com/repos/nette/di/zipball/942e406f63b88b57cb4e095ae0fd95c103d12c5b", + "reference": "942e406f63b88b57cb4e095ae0fd95c103d12c5b", "shasum": "" }, "require": { "ext-tokenizer": "*", - "nette/neon": "^3.0", + "nette/neon": "^3.3", "nette/php-generator": "^3.3.3", "nette/robot-loader": "^3.2", - "nette/schema": "^1.0", - "nette/utils": "^3.1", - "php": ">=7.1" + "nette/schema": "^1.1", + "nette/utils": "^3.1.6", + "php": ">=7.1 <8.2" }, "conflict": { "nette/bootstrap": "<3.0" @@ -1517,7 +1614,7 @@ "homepage": "https://nette.org/contributors" } ], - "description": "💎 Nette Dependency Injection Container: Flexible, compiled and full-featured DIC with perfectly usable autowiring and support for all new PHP 7.1 features.", + "description": "💎 Nette Dependency Injection Container: Flexible, compiled and full-featured DIC with perfectly usable autowiring and support for all new PHP features.", "homepage": "https://nette.org", "keywords": [ "compiled", @@ -1530,22 +1627,22 @@ ], "support": { "issues": "https://github.com/nette/di/issues", - "source": "https://github.com/nette/di/tree/master" + "source": "https://github.com/nette/di/tree/v3.0.11" }, - "time": "2020-08-13T13:04:23+00:00" + "time": "2021-10-26T11:44:44+00:00" }, { "name": "nette/finder", - "version": "v2.5.2", + "version": "v2.5.3", "source": { "type": "git", "url": "https://github.com/nette/finder.git", - "reference": "4ad2c298eb8c687dd0e74ae84206a4186eeaed50" + "reference": "64dc25b7929b731e72a1bc84a9e57727f5d5d3e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/finder/zipball/4ad2c298eb8c687dd0e74ae84206a4186eeaed50", - "reference": "4ad2c298eb8c687dd0e74ae84206a4186eeaed50", + "url": "https://api.github.com/repos/nette/finder/zipball/64dc25b7929b731e72a1bc84a9e57727f5d5d3e8", + "reference": "64dc25b7929b731e72a1bc84a9e57727f5d5d3e8", "shasum": "" }, "require": { @@ -1574,8 +1671,8 @@ "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause", - "GPL-2.0", - "GPL-3.0" + "GPL-2.0-only", + "GPL-3.0-only" ], "authors": [ { @@ -1597,38 +1694,40 @@ ], "support": { "issues": "https://github.com/nette/finder/issues", - "source": "https://github.com/nette/finder/tree/v2.5.2" + "source": "https://github.com/nette/finder/tree/v2.5.3" }, - "time": "2020-01-03T20:35:40+00:00" + "time": "2021-12-12T17:43:24+00:00" }, { "name": "nette/neon", - "version": "v3.2.1", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/nette/neon.git", - "reference": "a5b3a60833d2ef55283a82d0c30b45d136b29e75" + "reference": "54b287d8c2cdbe577b02e28ca1713e275b05ece2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/neon/zipball/a5b3a60833d2ef55283a82d0c30b45d136b29e75", - "reference": "a5b3a60833d2ef55283a82d0c30b45d136b29e75", + "url": "https://api.github.com/repos/nette/neon/zipball/54b287d8c2cdbe577b02e28ca1713e275b05ece2", + "reference": "54b287d8c2cdbe577b02e28ca1713e275b05ece2", "shasum": "" }, "require": { - "ext-iconv": "*", "ext-json": "*", "php": ">=7.1" }, "require-dev": { "nette/tester": "^2.0", "phpstan/phpstan": "^0.12", - "tracy/tracy": "^2.3" + "tracy/tracy": "^2.7" }, + "bin": [ + "bin/neon-lint" + ], "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -1663,33 +1762,33 @@ ], "support": { "issues": "https://github.com/nette/neon/issues", - "source": "https://github.com/nette/neon/tree/master" + "source": "https://github.com/nette/neon/tree/v3.3.2" }, - "time": "2020-07-31T12:28:05+00:00" + "time": "2021-11-25T15:57:41+00:00" }, { "name": "nette/php-generator", - "version": "v3.5.0", + "version": "v3.6.5", "source": { "type": "git", "url": "https://github.com/nette/php-generator.git", - "reference": "9162f7455059755dcbece1b5570d1bbfc6f0ab0d" + "reference": "9370403f9d9c25b51c4596ded1fbfe70347f7c82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/php-generator/zipball/9162f7455059755dcbece1b5570d1bbfc6f0ab0d", - "reference": "9162f7455059755dcbece1b5570d1bbfc6f0ab0d", + "url": "https://api.github.com/repos/nette/php-generator/zipball/9370403f9d9c25b51c4596ded1fbfe70347f7c82", + "reference": "9370403f9d9c25b51c4596ded1fbfe70347f7c82", "shasum": "" }, "require": { "nette/utils": "^3.1.2", - "php": ">=7.1" + "php": ">=7.2 <8.2" }, "require-dev": { - "nette/tester": "^2.0", - "nikic/php-parser": "^4.4", + "nette/tester": "^2.4", + "nikic/php-parser": "^4.13", "phpstan/phpstan": "^0.12", - "tracy/tracy": "^2.3" + "tracy/tracy": "^2.8" }, "suggest": { "nikic/php-parser": "to use ClassType::withBodiesFrom() & GlobalFunction::withBodyFrom()" @@ -1697,7 +1796,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.5-dev" + "dev-master": "3.6-dev" } }, "autoload": { @@ -1721,7 +1820,7 @@ "homepage": "https://nette.org/contributors" } ], - "description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 7.4 features.", + "description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 8.1 features.", "homepage": "https://nette.org", "keywords": [ "code", @@ -1731,22 +1830,22 @@ ], "support": { "issues": "https://github.com/nette/php-generator/issues", - "source": "https://github.com/nette/php-generator/tree/v3.5.0" + "source": "https://github.com/nette/php-generator/tree/v3.6.5" }, - "time": "2020-11-02T16:16:58+00:00" + "time": "2021-11-24T16:23:44+00:00" }, { "name": "nette/robot-loader", - "version": "v3.3.1", + "version": "v3.4.1", "source": { "type": "git", "url": "https://github.com/nette/robot-loader.git", - "reference": "15c1ecd0e6e69e8d908dfc4cca7b14f3b850a96b" + "reference": "e2adc334cb958164c050f485d99c44c430f51fe2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/robot-loader/zipball/15c1ecd0e6e69e8d908dfc4cca7b14f3b850a96b", - "reference": "15c1ecd0e6e69e8d908dfc4cca7b14f3b850a96b", + "url": "https://api.github.com/repos/nette/robot-loader/zipball/e2adc334cb958164c050f485d99c44c430f51fe2", + "reference": "e2adc334cb958164c050f485d99c44c430f51fe2", "shasum": "" }, "require": { @@ -1763,7 +1862,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.3-dev" + "dev-master": "3.4-dev" } }, "autoload": { @@ -1798,36 +1897,38 @@ ], "support": { "issues": "https://github.com/nette/robot-loader/issues", - "source": "https://github.com/nette/robot-loader/tree/v3.3.1" + "source": "https://github.com/nette/robot-loader/tree/v3.4.1" }, - "time": "2020-09-15T15:14:17+00:00" + "time": "2021-08-25T15:53:54+00:00" }, { "name": "nette/schema", - "version": "v1.0.2", + "version": "v1.2.2", "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "febf71fb4052c824046f5a33f4f769a6e7fa0cb4" + "reference": "9a39cef03a5b34c7de64f551538cbba05c2be5df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/febf71fb4052c824046f5a33f4f769a6e7fa0cb4", - "reference": "febf71fb4052c824046f5a33f4f769a6e7fa0cb4", + "url": "https://api.github.com/repos/nette/schema/zipball/9a39cef03a5b34c7de64f551538cbba05c2be5df", + "reference": "9a39cef03a5b34c7de64f551538cbba05c2be5df", "shasum": "" }, "require": { - "nette/utils": "^3.1", - "php": ">=7.1" + "nette/utils": "^2.5.7 || ^3.1.5 || ^4.0", + "php": ">=7.1 <8.2" }, "require-dev": { - "nette/tester": "^2.2", + "nette/tester": "^2.3 || ^2.4", "phpstan/phpstan-nette": "^0.12", - "tracy/tracy": "^2.3" + "tracy/tracy": "^2.7" }, "type": "library", "extra": { - "branch-alias": [] + "branch-alias": { + "dev-master": "1.2-dev" + } }, "autoload": { "classmap": [ @@ -1837,8 +1938,8 @@ "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause", - "GPL-2.0", - "GPL-3.0" + "GPL-2.0-only", + "GPL-3.0-only" ], "authors": [ { @@ -1858,30 +1959,33 @@ ], "support": { "issues": "https://github.com/nette/schema/issues", - "source": "https://github.com/nette/schema/tree/v1.0.2" + "source": "https://github.com/nette/schema/tree/v1.2.2" }, - "time": "2020-01-06T22:52:48+00:00" + "time": "2021-10-15T11:40:02+00:00" }, { "name": "nette/utils", - "version": "v3.1.3", + "version": "v3.2.6", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "c09937fbb24987b2a41c6022ebe84f4f1b8eec0f" + "reference": "2f261e55bd6a12057442045bf2c249806abc1d02" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/c09937fbb24987b2a41c6022ebe84f4f1b8eec0f", - "reference": "c09937fbb24987b2a41c6022ebe84f4f1b8eec0f", + "url": "https://api.github.com/repos/nette/utils/zipball/2f261e55bd6a12057442045bf2c249806abc1d02", + "reference": "2f261e55bd6a12057442045bf2c249806abc1d02", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2 <8.2" + }, + "conflict": { + "nette/di": "<3.0.6" }, "require-dev": { "nette/tester": "~2.0", - "phpstan/phpstan": "^0.12", + "phpstan/phpstan": "^1.0", "tracy/tracy": "^2.3" }, "suggest": { @@ -1896,7 +2000,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -1920,7 +2024,7 @@ "homepage": "https://nette.org/contributors" } ], - "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", "homepage": "https://nette.org", "keywords": [ "array", @@ -1940,22 +2044,22 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v3.1.3" + "source": "https://github.com/nette/utils/tree/v3.2.6" }, - "time": "2020-08-07T10:34:21+00:00" + "time": "2021-11-24T15:47:23+00:00" }, { "name": "nikic/php-parser", - "version": "v4.10.5", + "version": "v4.13.2", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "4432ba399e47c66624bc73c8c0f811e5c109576f" + "reference": "210577fe3cf7badcc5814d99455df46564f3c077" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4432ba399e47c66624bc73c8c0f811e5c109576f", - "reference": "4432ba399e47c66624bc73c8c0f811e5c109576f", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/210577fe3cf7badcc5814d99455df46564f3c077", + "reference": "210577fe3cf7badcc5814d99455df46564f3c077", "shasum": "" }, "require": { @@ -1996,9 +2100,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.10.5" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.13.2" }, - "time": "2021-05-03T19:11:20+00:00" + "time": "2021-11-30T19:35:32+00:00" }, { "name": "ondram/ci-detector", @@ -2074,37 +2178,37 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "4.3.60", + "version": "5.0.7.2", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "0a94d3041d5801da091d0ecce0ef4e077f83467a" + "reference": "62e6d33070d670cf7385b248371179cb3b552c2d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/0a94d3041d5801da091d0ecce0ef4e077f83467a", - "reference": "0a94d3041d5801da091d0ecce0ef4e077f83467a", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/62e6d33070d670cf7385b248371179cb3b552c2d", + "reference": "62e6d33070d670cf7385b248371179cb3b552c2d", "shasum": "" }, "require": { "ext-json": "*", - "jetbrains/phpstorm-stubs": "dev-master#0a73df114cdea7f30c8b5f6fbfbf8e6839a89e88", - "nikic/php-parser": "4.10.5", - "php": ">=7.1.0" + "jetbrains/phpstorm-stubs": "dev-master#3a6d6053bcc6c9a154827b2624e10b5c428b7eb0", + "nikic/php-parser": "^4.13.2", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "thecodingmachine/safe": "<1.1.3" }, "require-dev": { - "phpstan/phpstan": "^0.12.49", - "phpunit/phpunit": "^7.5.18" + "doctrine/coding-standard": "^9.0.0", + "phpstan/phpstan": "^1.3.0", + "phpunit/phpunit": "^9.5.11", + "rector/rector": "0.12.5" }, "suggest": { "composer/composer": "Required to use the ComposerSourceLocator" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, "autoload": { "psr-4": { "PHPStan\\BetterReflection\\": "src" @@ -2138,22 +2242,22 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/4.3.60" + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/5.0.7.2" }, - "time": "2021-06-17T17:55:18+00:00" + "time": "2022-02-25T19:46:30+00:00" }, { "name": "phpstan/php-8-stubs", - "version": "0.1.21", + "version": "0.1.49", "source": { "type": "git", "url": "https://github.com/phpstan/php-8-stubs.git", - "reference": "687165b0b4b0ef908278c03e65b2e1c05ecf8e92" + "reference": "095a11fc8ba747bd200fc91701ef1c824818bf49" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/687165b0b4b0ef908278c03e65b2e1c05ecf8e92", - "reference": "687165b0b4b0ef908278c03e65b2e1c05ecf8e92", + "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/095a11fc8ba747bd200fc91701ef1c824818bf49", + "reference": "095a11fc8ba747bd200fc91701ef1c824818bf49", "shasum": "" }, "type": "library", @@ -2170,22 +2274,22 @@ "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.1.21" + "source": "https://github.com/phpstan/php-8-stubs/tree/0.1.49" }, - "time": "2021-06-06T16:07:43+00:00" + "time": "2022-03-11T00:14:16+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "0.5.5", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "ea0b17460ec38e20d7eb64e7ec49b5d44af5d28c" + "reference": "dbc093d7af60eff5cd575d2ed761b15ed40bd08e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/ea0b17460ec38e20d7eb64e7ec49b5d44af5d28c", - "reference": "ea0b17460ec38e20d7eb64e7ec49b5d44af5d28c", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/dbc093d7af60eff5cd575d2ed761b15ed40bd08e", + "reference": "dbc093d7af60eff5cd575d2ed761b15ed40bd08e", "shasum": "" }, "require": { @@ -2194,15 +2298,15 @@ "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^0.12.87", - "phpstan/phpstan-strict-rules": "^0.12.5", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", "phpunit/phpunit": "^9.5", "symfony/process": "^5.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "0.5-dev" + "dev-master": "1.0-dev" } }, "autoload": { @@ -2219,9 +2323,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/0.5.5" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.2.0" }, - "time": "2021-06-11T13:24:46+00:00" + "time": "2021-09-16T20:46:02+00:00" }, { "name": "psr/container", @@ -2376,16 +2480,16 @@ }, { "name": "react/cache", - "version": "v1.1.0", + "version": "v1.1.1", "source": { "type": "git", "url": "https://github.com/reactphp/cache.git", - "reference": "44a568925556b0bd8cacc7b49fb0f1cf0d706a0c" + "reference": "4bf736a2cccec7298bdf745db77585966fc2ca7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/cache/zipball/44a568925556b0bd8cacc7b49fb0f1cf0d706a0c", - "reference": "44a568925556b0bd8cacc7b49fb0f1cf0d706a0c", + "url": "https://api.github.com/repos/reactphp/cache/zipball/4bf736a2cccec7298bdf745db77585966fc2ca7e", + "reference": "4bf736a2cccec7298bdf745db77585966fc2ca7e", "shasum": "" }, "require": { @@ -2436,7 +2540,7 @@ ], "support": { "issues": "https://github.com/reactphp/cache/issues", - "source": "https://github.com/reactphp/cache/tree/v1.1.0" + "source": "https://github.com/reactphp/cache/tree/v1.1.1" }, "funding": [ { @@ -2448,32 +2552,32 @@ "type": "github" } ], - "time": "2020-09-18T12:12:35+00:00" + "time": "2021-02-02T06:47:52+00:00" }, { "name": "react/child-process", - "version": "v0.6.1", + "version": "v0.6.4", "source": { "type": "git", "url": "https://github.com/reactphp/child-process.git", - "reference": "6895afa583d51dc10a4b9e93cd3bce17b3b77ac3" + "reference": "a778f3fb828d68caf8a9ab6567fd8342a86f12fe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/child-process/zipball/6895afa583d51dc10a4b9e93cd3bce17b3b77ac3", - "reference": "6895afa583d51dc10a4b9e93cd3bce17b3b77ac3", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/a778f3fb828d68caf8a9ab6567fd8342a86f12fe", + "reference": "a778f3fb828d68caf8a9ab6567fd8342a86f12fe", "shasum": "" }, "require": { "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "php": ">=5.3.0", - "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", - "react/stream": "^1.0 || ^0.7.6" + "react/event-loop": "^1.2", + "react/stream": "^1.2" }, "require-dev": { - "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35", - "react/socket": "^1.0", - "sebastian/environment": "^3.0 || ^2.0 || ^1.0" + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", + "react/socket": "^1.8", + "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" }, "type": "library", "autoload": { @@ -2485,6 +2589,28 @@ "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": "Event-driven library for executing child processes with ReactPHP.", "keywords": [ "event-driven", @@ -2493,28 +2619,38 @@ ], "support": { "issues": "https://github.com/reactphp/child-process/issues", - "source": "https://github.com/reactphp/child-process/tree/v0.6.1" + "source": "https://github.com/reactphp/child-process/tree/v0.6.4" }, - "time": "2019-02-15T13:48:16+00:00" + "funding": [ + { + "url": "https://github.com/WyriHaximus", + "type": "github" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2021-10-12T10:37:07+00:00" }, { "name": "react/dns", - "version": "v1.4.0", + "version": "v1.8.0", "source": { "type": "git", "url": "https://github.com/reactphp/dns.git", - "reference": "665260757171e2ab17485b44e7ffffa7acb6ca1f" + "reference": "2a5a74ab751e53863b45fb87e1d3913884f88248" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/dns/zipball/665260757171e2ab17485b44e7ffffa7acb6ca1f", - "reference": "665260757171e2ab17485b44e7ffffa7acb6ca1f", + "url": "https://api.github.com/repos/reactphp/dns/zipball/2a5a74ab751e53863b45fb87e1d3913884f88248", + "reference": "2a5a74ab751e53863b45fb87e1d3913884f88248", "shasum": "" }, "require": { "php": ">=5.3.0", "react/cache": "^1.0 || ^0.6 || ^0.5", - "react/event-loop": "^1.0 || ^0.5", + "react/event-loop": "^1.2", "react/promise": "^3.0 || ^2.7 || ^1.2.1", "react/promise-timer": "^1.2" }, @@ -2563,7 +2699,7 @@ ], "support": { "issues": "https://github.com/reactphp/dns/issues", - "source": "https://github.com/reactphp/dns/tree/v1.4.0" + "source": "https://github.com/reactphp/dns/tree/v1.8.0" }, "funding": [ { @@ -2575,27 +2711,27 @@ "type": "github" } ], - "time": "2020-09-18T12:12:55+00:00" + "time": "2021-07-11T12:40:34+00:00" }, { "name": "react/event-loop", - "version": "v1.1.1", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/reactphp/event-loop.git", - "reference": "6d24de090cd59cfc830263cfba965be77b563c13" + "reference": "be6dee480fc4692cec0504e65eb486e3be1aa6f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/event-loop/zipball/6d24de090cd59cfc830263cfba965be77b563c13", - "reference": "6d24de090cd59cfc830263cfba965be77b563c13", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/be6dee480fc4692cec0504e65eb486e3be1aa6f2", + "reference": "be6dee480fc4692cec0504e65eb486e3be1aa6f2", "shasum": "" }, "require": { "php": ">=5.3.0" }, "require-dev": { - "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35" + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" }, "suggest": { "ext-event": "~1.0 for ExtEventLoop", @@ -2612,6 +2748,28 @@ "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": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", "keywords": [ "asynchronous", @@ -2619,33 +2777,43 @@ ], "support": { "issues": "https://github.com/reactphp/event-loop/issues", - "source": "https://github.com/reactphp/event-loop/tree/v1.1.1" + "source": "https://github.com/reactphp/event-loop/tree/v1.2.0" }, - "time": "2020-01-01T18:39:52+00:00" + "funding": [ + { + "url": "https://github.com/WyriHaximus", + "type": "github" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2021-07-11T12:31:24+00:00" }, { "name": "react/http", - "version": "v1.1.0", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/reactphp/http.git", - "reference": "754b0c18545d258922ffa907f3b18598280fdecd" + "reference": "8a0fd7c0aa74f0db3008b1e47ca86c613cbb040e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/http/zipball/754b0c18545d258922ffa907f3b18598280fdecd", - "reference": "754b0c18545d258922ffa907f3b18598280fdecd", + "url": "https://api.github.com/repos/reactphp/http/zipball/8a0fd7c0aa74f0db3008b1e47ca86c613cbb040e", + "reference": "8a0fd7c0aa74f0db3008b1e47ca86c613cbb040e", "shasum": "" }, "require": { "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "php": ">=5.3.0", "psr/http-message": "^1.0", - "react/event-loop": "^1.0 || ^0.5", + "react/event-loop": "^1.2", "react/promise": "^2.3 || ^1.2.1", "react/promise-stream": "^1.1", - "react/socket": "^1.6", - "react/stream": "^1.1", + "react/socket": "^1.9", + "react/stream": "^1.2", "ringcentral/psr7": "^1.2" }, "require-dev": { @@ -2703,7 +2871,7 @@ ], "support": { "issues": "https://github.com/reactphp/http/issues", - "source": "https://github.com/reactphp/http/tree/v1.1.0" + "source": "https://github.com/reactphp/http/tree/v1.5.0" }, "funding": [ { @@ -2715,7 +2883,7 @@ "type": "github" } ], - "time": "2020-09-11T11:01:51+00:00" + "time": "2021-08-04T12:24:55+00:00" }, { "name": "react/promise", @@ -2739,12 +2907,12 @@ }, "type": "library", "autoload": { - "psr-4": { - "React\\Promise\\": "src/" - }, "files": [ "src/functions_include.php" - ] + ], + "psr-4": { + "React\\Promise\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2769,16 +2937,16 @@ }, { "name": "react/promise-stream", - "version": "v1.2.0", + "version": "v1.3.0", "source": { "type": "git", "url": "https://github.com/reactphp/promise-stream.git", - "reference": "6384d8b76cf7dcc44b0bf3343fb2b2928412d1fe" + "reference": "3ebd94fe0d8edbf44937948af28d02d5437e9949" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise-stream/zipball/6384d8b76cf7dcc44b0bf3343fb2b2928412d1fe", - "reference": "6384d8b76cf7dcc44b0bf3343fb2b2928412d1fe", + "url": "https://api.github.com/repos/reactphp/promise-stream/zipball/3ebd94fe0d8edbf44937948af28d02d5437e9949", + "reference": "3ebd94fe0d8edbf44937948af28d02d5437e9949", "shasum": "" }, "require": { @@ -2788,18 +2956,18 @@ }, "require-dev": { "clue/block-react": "^1.0", - "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35", + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3", "react/promise-timer": "^1.0" }, "type": "library", "autoload": { - "psr-4": { - "React\\Promise\\Stream\\": "src/" - }, "files": [ "src/functions_include.php" - ] + ], + "psr-4": { + "React\\Promise\\Stream\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2808,7 +2976,23 @@ "authors": [ { "name": "Christian Lück", - "email": "christian@lueck.tv" + "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", @@ -2823,40 +3007,50 @@ ], "support": { "issues": "https://github.com/reactphp/promise-stream/issues", - "source": "https://github.com/reactphp/promise-stream/tree/v1.2.0" + "source": "https://github.com/reactphp/promise-stream/tree/v1.3.0" }, - "time": "2019-07-03T12:29:10+00:00" + "funding": [ + { + "url": "https://github.com/WyriHaximus", + "type": "github" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2021-10-18T10:47:09+00:00" }, { "name": "react/promise-timer", - "version": "v1.6.0", + "version": "v1.8.0", "source": { "type": "git", "url": "https://github.com/reactphp/promise-timer.git", - "reference": "daee9baf6ef30c43ea4c86399f828bb5f558f6e6" + "reference": "0bbbcc79589e5bfdddba68a287f1cb805581a479" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise-timer/zipball/daee9baf6ef30c43ea4c86399f828bb5f558f6e6", - "reference": "daee9baf6ef30c43ea4c86399f828bb5f558f6e6", + "url": "https://api.github.com/repos/reactphp/promise-timer/zipball/0bbbcc79589e5bfdddba68a287f1cb805581a479", + "reference": "0bbbcc79589e5bfdddba68a287f1cb805581a479", "shasum": "" }, "require": { "php": ">=5.3", - "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", + "react/event-loop": "^1.2", "react/promise": "^3.0 || ^2.7.0 || ^1.2.1" }, "require-dev": { - "phpunit/phpunit": "^9.0 || ^5.7 || ^4.8.35" + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" }, "type": "library", "autoload": { - "psr-4": { - "React\\Promise\\Timer\\": "src/" - }, "files": [ "src/functions_include.php" - ] + ], + "psr-4": { + "React\\Promise\\Timer\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2865,7 +3059,23 @@ "authors": [ { "name": "Christian Lück", - "email": "christian@lueck.tv" + "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": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.", @@ -2880,32 +3090,42 @@ ], "support": { "issues": "https://github.com/reactphp/promise-timer/issues", - "source": "https://github.com/reactphp/promise-timer/tree/v1.6.0" + "source": "https://github.com/reactphp/promise-timer/tree/v1.8.0" }, - "time": "2020-07-10T12:18:06+00:00" - }, - { - "name": "react/socket", - "version": "v1.6.0", - "source": { + "funding": [ + { + "url": "https://github.com/WyriHaximus", + "type": "github" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2021-12-06T11:08:48+00:00" + }, + { + "name": "react/socket", + "version": "v1.10.0", + "source": { "type": "git", "url": "https://github.com/reactphp/socket.git", - "reference": "e2b96b23a13ca9b41ab343268dbce3f8ef4d524a" + "reference": "d132fde589ea97f4165f2d94b5296499eac125ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/socket/zipball/e2b96b23a13ca9b41ab343268dbce3f8ef4d524a", - "reference": "e2b96b23a13ca9b41ab343268dbce3f8ef4d524a", + "url": "https://api.github.com/repos/reactphp/socket/zipball/d132fde589ea97f4165f2d94b5296499eac125ec", + "reference": "d132fde589ea97f4165f2d94b5296499eac125ec", "shasum": "" }, "require": { "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "php": ">=5.3.0", - "react/dns": "^1.1", - "react/event-loop": "^1.0 || ^0.5", + "react/dns": "^1.8", + "react/event-loop": "^1.2", "react/promise": "^2.6.0 || ^1.2.1", "react/promise-timer": "^1.4.0", - "react/stream": "^1.1" + "react/stream": "^1.2" }, "require-dev": { "clue/block-react": "^1.2", @@ -2954,7 +3174,7 @@ ], "support": { "issues": "https://github.com/reactphp/socket/issues", - "source": "https://github.com/reactphp/socket/tree/v1.6.0" + "source": "https://github.com/reactphp/socket/tree/v1.10.0" }, "funding": [ { @@ -2966,30 +3186,30 @@ "type": "github" } ], - "time": "2020-08-28T12:49:05+00:00" + "time": "2021-11-29T10:08:24+00:00" }, { "name": "react/stream", - "version": "v1.1.1", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/reactphp/stream.git", - "reference": "7c02b510ee3f582c810aeccd3a197b9c2f52ff1a" + "reference": "7a423506ee1903e89f1e08ec5f0ed430ff784ae9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/stream/zipball/7c02b510ee3f582c810aeccd3a197b9c2f52ff1a", - "reference": "7c02b510ee3f582c810aeccd3a197b9c2f52ff1a", + "url": "https://api.github.com/repos/reactphp/stream/zipball/7a423506ee1903e89f1e08ec5f0ed430ff784ae9", + "reference": "7a423506ee1903e89f1e08ec5f0ed430ff784ae9", "shasum": "" }, "require": { "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "php": ">=5.3.8", - "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5" + "react/event-loop": "^1.2" }, "require-dev": { "clue/stream-filter": "~1.2", - "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35" + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" }, "type": "library", "autoload": { @@ -3001,6 +3221,28 @@ "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": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", "keywords": [ "event-driven", @@ -3014,9 +3256,19 @@ ], "support": { "issues": "https://github.com/reactphp/stream/issues", - "source": "https://github.com/reactphp/stream/tree/v1.1.1" + "source": "https://github.com/reactphp/stream/tree/v1.2.0" }, - "time": "2020-05-04T10:17:57+00:00" + "funding": [ + { + "url": "https://github.com/WyriHaximus", + "type": "github" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2021-07-11T12:37:55+00:00" }, { "name": "ringcentral/psr7", @@ -3049,12 +3301,12 @@ } }, "autoload": { - "psr-4": { - "RingCentral\\Psr7\\": "src/" - }, "files": [ "src/functions_include.php" - ] + ], + "psr-4": { + "RingCentral\\Psr7\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3081,42 +3333,46 @@ }, { "name": "symfony/console", - "version": "v4.4.21", + "version": "v5.4.3", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "1ba4560dbbb9fcf5ae28b61f71f49c678086cf23" + "reference": "a2a86ec353d825c75856c6fd14fac416a7bdb6b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/1ba4560dbbb9fcf5ae28b61f71f49c678086cf23", - "reference": "1ba4560dbbb9fcf5ae28b61f71f49c678086cf23", + "url": "https://api.github.com/repos/symfony/console/zipball/a2a86ec353d825c75856c6fd14fac416a7bdb6b8", + "reference": "a2a86ec353d825c75856c6fd14fac416a7bdb6b8", "shasum": "" }, "require": { - "php": ">=7.1.3", + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php73": "^1.8", - "symfony/polyfill-php80": "^1.15", - "symfony/service-contracts": "^1.1|^2" + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/string": "^5.1|^6.0" }, "conflict": { - "symfony/dependency-injection": "<3.4", - "symfony/event-dispatcher": "<4.3|>=5", + "psr/log": ">=3", + "symfony/dependency-injection": "<4.4", + "symfony/dotenv": "<5.1", + "symfony/event-dispatcher": "<4.4", "symfony/lock": "<4.4", - "symfony/process": "<3.3" + "symfony/process": "<4.4" }, "provide": { - "psr/log-implementation": "1.0" + "psr/log-implementation": "1.0|2.0" }, "require-dev": { - "psr/log": "~1.0", - "symfony/config": "^3.4|^4.0|^5.0", - "symfony/dependency-injection": "^3.4|^4.0|^5.0", - "symfony/event-dispatcher": "^4.3", - "symfony/lock": "^4.4|^5.0", - "symfony/process": "^3.4|^4.0|^5.0", - "symfony/var-dumper": "^4.3|^5.0" + "psr/log": "^1|^2", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/lock": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0" }, "suggest": { "psr/log": "For using the console logger", @@ -3149,8 +3405,81 @@ ], "description": "Eases the creation of beautiful and testable command line interfaces", "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v5.4.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-26T16:28:35+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/6f981ee24cf69ee7ce9736146d1c57c2780598a8", + "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/console/tree/v4.4.21" + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.0" }, "funding": [ { @@ -3166,24 +3495,26 @@ "type": "tidelift" } ], - "time": "2021-03-26T09:23:24+00:00" + "time": "2021-07-12T14:48:14+00:00" }, { "name": "symfony/finder", - "version": "v4.4.16", + "version": "v5.4.3", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "26f63b8d4e92f2eecd90f6791a563ebb001abe31" + "reference": "231313534dded84c7ecaa79d14bc5da4ccb69b7d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/26f63b8d4e92f2eecd90f6791a563ebb001abe31", - "reference": "26f63b8d4e92f2eecd90f6791a563ebb001abe31", + "url": "https://api.github.com/repos/symfony/finder/zipball/231313534dded84c7ecaa79d14bc5da4ccb69b7d", + "reference": "231313534dded84c7ecaa79d14bc5da4ccb69b7d", "shasum": "" }, "require": { - "php": ">=7.1.3" + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-php80": "^1.16" }, "type": "library", "autoload": { @@ -3208,10 +3539,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Finder Component", + "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v4.4.16" + "source": "https://github.com/symfony/finder/tree/v5.4.3" }, "funding": [ { @@ -3227,32 +3558,35 @@ "type": "tidelift" } ], - "time": "2020-10-24T11:50:19+00:00" + "time": "2022-01-26T16:34:36+00:00" }, { - "name": "symfony/polyfill-mbstring", - "version": "v1.22.1", + "name": "symfony/polyfill-ctype", + "version": "v1.24.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "5232de97ee3b75b0360528dae24e73db49566ab1" + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "30885182c981ab175d4d034db0f6f469898070ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/5232de97ee3b75b0360528dae24e73db49566ab1", - "reference": "5232de97ee3b75b0360528dae24e73db49566ab1", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", + "reference": "30885182c981ab175d4d034db0f6f469898070ab", "shasum": "" }, "require": { "php": ">=7.1" }, + "provide": { + "ext-ctype": "*" + }, "suggest": { - "ext-mbstring": "For best performance" + "ext-ctype": "For best performance" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.22-dev" + "dev-main": "1.23-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3260,12 +3594,91 @@ } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.24.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-10-20T20:35:02+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.24.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "81b86b50cf841a64252b439e738e97f4a34e2783" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/81b86b50cf841a64252b439e738e97f4a34e2783", + "reference": "81b86b50cf841a64252b439e738e97f4a34e2783", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3281,17 +3694,18 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for the Mbstring extension", + "description": "Symfony polyfill for intl's grapheme_* functions", "homepage": "https://symfony.com", "keywords": [ "compatibility", - "mbstring", + "grapheme", + "intl", "polyfill", "portable", "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.22.1" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.24.0" }, "funding": [ { @@ -3307,29 +3721,32 @@ "type": "tidelift" } ], - "time": "2021-01-22T09:19:47+00:00" + "time": "2021-11-23T21:10:46+00:00" }, { - "name": "symfony/polyfill-php73", - "version": "v1.22.1", + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.24.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2" + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", - "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", + "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", "shasum": "" }, "require": { "php": ">=7.1" }, + "suggest": { + "ext-intl": "For best performance" + }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.22-dev" + "dev-main": "1.23-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3337,12 +3754,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -3361,16 +3778,18 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "description": "Symfony polyfill for intl's Normalizer class and related functions", "homepage": "https://symfony.com", "keywords": [ "compatibility", + "intl", + "normalizer", "polyfill", "portable", "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.22.1" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.24.0" }, "funding": [ { @@ -3386,29 +3805,35 @@ "type": "tidelift" } ], - "time": "2021-01-07T16:49:33+00:00" + "time": "2021-02-19T12:13:01+00:00" }, { - "name": "symfony/polyfill-php80", - "version": "v1.22.1", + "name": "symfony/polyfill-mbstring", + "version": "v1.24.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91" + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dc3063ba22c2a1fd2f45ed856374d79114998f91", - "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825", + "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825", "shasum": "" }, "require": { "php": ">=7.1" }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.22-dev" + "dev-main": "1.23-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3416,15 +3841,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, "files": [ "bootstrap.php" ], - "classmap": [ - "Resources/stubs" - ] + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3432,9 +3854,467 @@ ], "authors": [ { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.24.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-11-30T18:21:41+00:00" + }, + { + "name": "symfony/polyfill-php72", + "version": "v1.24.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "9a142215a36a3888e30d0a9eeea9766764e96976" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/9a142215a36a3888e30d0a9eeea9766764e96976", + "reference": "9a142215a36a3888e30d0a9eeea9766764e96976", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php72/tree/v1.24.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-05-27T09:17:38+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.24.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/cc5db0e22b3cb4111010e48785a97f670b350ca5", + "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.24.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-06-05T21:20:04+00:00" + }, + { + "name": "symfony/polyfill-php74", + "version": "v1.24.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php74.git", + "reference": "a5d80cdf049bd3b0af6da91184a2cd37533c0fd8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php74/zipball/a5d80cdf049bd3b0af6da91184a2cd37533c0fd8", + "reference": "a5d80cdf049bd3b0af6da91184a2cd37533c0fd8", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "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" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php74/tree/v1.24.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-02-19T12:13:01+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.24.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "57b712b08eddb97c762a8caa32c84e037892d2e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/57b712b08eddb97c762a8caa32c84e037892d2e9", + "reference": "57b712b08eddb97c762a8caa32c84e037892d2e9", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.24.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-09-13T13:58:33+00:00" + }, + { + "name": "symfony/process", + "version": "v5.4.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "553f50487389a977eb31cf6b37faae56da00f753" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/553f50487389a977eb31cf6b37faae56da00f753", + "reference": "553f50487389a977eb31cf6b37faae56da00f753", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-php80": "^1.16" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v5.4.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-26T16:28:35+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v2.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc", + "reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1", + "symfony/deprecation-contracts": "^2.1" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -3444,16 +4324,18 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "description": "Generic abstractions related to writing services", "homepage": "https://symfony.com", "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.22.1" + "source": "https://github.com/symfony/service-contracts/tree/v2.5.0" }, "funding": [ { @@ -3469,39 +4351,50 @@ "type": "tidelift" } ], - "time": "2021-01-07T16:49:33+00:00" + "time": "2021-11-04T16:48:04+00:00" }, { - "name": "symfony/service-contracts", - "version": "v1.1.8", + "name": "symfony/string", + "version": "v5.4.3", "source": { "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "ffc7f5692092df31515df2a5ecf3b7302b3ddacf" + "url": "https://github.com/symfony/string.git", + "reference": "92043b7d8383e48104e411bc9434b260dbeb5a10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/ffc7f5692092df31515df2a5ecf3b7302b3ddacf", - "reference": "ffc7f5692092df31515df2a5ecf3b7302b3ddacf", + "url": "https://api.github.com/repos/symfony/string/zipball/92043b7d8383e48104e411bc9434b260dbeb5a10", + "reference": "92043b7d8383e48104e411bc9434b260dbeb5a10", "shasum": "" }, "require": { - "php": "^7.1.3", - "psr/container": "^1.0" + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "~1.15" }, - "suggest": { - "symfony/service-implementation": "" + "conflict": { + "symfony/translation-contracts": ">=3.0" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - } + "require-dev": { + "symfony/error-handler": "^4.4|^5.0|^6.0", + "symfony/http-client": "^4.4|^5.0|^6.0", + "symfony/translation-contracts": "^1.1|^2", + "symfony/var-exporter": "^4.4|^5.0|^6.0" }, + "type": "library", "autoload": { + "files": [ + "Resources/functions.php" + ], "psr-4": { - "Symfony\\Contracts\\Service\\": "" - } + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3517,20 +4410,34 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Generic abstractions related to writing services", + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", "homepage": "https://symfony.com", "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v1.1.8" + "source": "https://github.com/symfony/string/tree/v5.4.3" }, - "time": "2019-10-14T12:27:06+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:53:40+00:00" } ], "packages-dev": [ @@ -3756,9 +4663,6 @@ "require": { "php": "^7.1 || ^8.0" }, - "replace": { - "myclabs/deep-copy": "self.version" - }, "require-dev": { "doctrine/collections": "^1.0", "doctrine/common": "^2.6", @@ -3766,12 +4670,12 @@ }, "type": "library", "autoload": { - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - }, "files": [ "src/DeepCopy/deep_copy.php" - ] + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4246,36 +5150,31 @@ }, { "name": "phpstan/phpstan-deprecation-rules", - "version": "0.12.5", + "version": "1.0.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", - "reference": "bfabc6a1b4617fbcbff43f03a4c04eae9bafae21" + "reference": "e5ccafb0dd8d835dd65d8d7a1a0d2b1b75414682" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/bfabc6a1b4617fbcbff43f03a4c04eae9bafae21", - "reference": "bfabc6a1b4617fbcbff43f03a4c04eae9bafae21", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/e5ccafb0dd8d835dd65d8d7a1a0d2b1b75414682", + "reference": "e5ccafb0dd8d835dd65d8d7a1a0d2b1b75414682", "shasum": "" }, "require": { "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^0.12.26" + "phpstan/phpstan": "^1.0" }, "require-dev": { - "consistence/coding-standard": "^3.0.1", - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", - "ergebnis/composer-normalize": "^2.0.2", - "jakub-onderka/php-parallel-lint": "^1.0", - "phing/phing": "^2.16.0", - "phpstan/phpstan-phpunit": "^0.12", - "phpunit/phpunit": "^7.0", - "slevomat/coding-standard": "^4.5.2" + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.5" }, "type": "phpstan-extension", "extra": { "branch-alias": { - "dev-master": "0.12-dev" + "dev-master": "1.0-dev" }, "phpstan": { "includes": [ @@ -4295,27 +5194,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/0.12.5" + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/1.0.0" }, - "time": "2020-07-21T14:52:30+00:00" + "time": "2021-09-23T11:02:21+00:00" }, { "name": "phpstan/phpstan-nette", - "version": "dev-master", + "version": "1.0.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-nette.git", - "reference": "9feb3cafff0f1d3bba38f0e8680085089deceb62" + "reference": "f4654b27b107241e052755ec187a0b1964541ba6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-nette/zipball/9feb3cafff0f1d3bba38f0e8680085089deceb62", - "reference": "9feb3cafff0f1d3bba38f0e8680085089deceb62", + "url": "https://api.github.com/repos/phpstan/phpstan-nette/zipball/f4654b27b107241e052755ec187a0b1964541ba6", + "reference": "f4654b27b107241e052755ec187a0b1964541ba6", "shasum": "" }, "require": { "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^0.12.86" + "phpstan/phpstan": "^1.0" }, "conflict": { "nette/application": "<2.3.0", @@ -4328,18 +5227,17 @@ "require-dev": { "nette/forms": "^3.0", "nette/utils": "^2.3.0 || ^3.0.0", - "phing/phing": "^2.16.3", + "nikic/php-parser": "^4.13.0", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-php-parser": "^0.12.2", - "phpstan/phpstan-phpunit": "^0.12.16", - "phpstan/phpstan-strict-rules": "^0.12.5", - "phpunit/phpunit": "^7.5.20" + "phpstan/phpstan-php-parser": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5" }, - "default-branch": true, "type": "phpstan-extension", "extra": { "branch-alias": { - "dev-master": "0.12-dev" + "dev-master": "1.0-dev" }, "phpstan": { "includes": [ @@ -4360,43 +5258,38 @@ "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/master" + "source": "https://github.com/phpstan/phpstan-nette/tree/1.0.0" }, - "time": "2021-04-30T11:16:08+00:00" + "time": "2021-09-20T16:12:57+00:00" }, { "name": "phpstan/phpstan-php-parser", - "version": "0.12.2", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-php-parser.git", - "reference": "0b27eec1e92d48fa82199844dec119b1b22baba0" + "reference": "1c7670dd92da864b5d019f22d9f512a6ae18b78e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-php-parser/zipball/0b27eec1e92d48fa82199844dec119b1b22baba0", - "reference": "0b27eec1e92d48fa82199844dec119b1b22baba0", + "url": "https://api.github.com/repos/phpstan/phpstan-php-parser/zipball/1c7670dd92da864b5d019f22d9f512a6ae18b78e", + "reference": "1c7670dd92da864b5d019f22d9f512a6ae18b78e", "shasum": "" }, "require": { "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^0.12" + "phpstan/phpstan": "^1.3" }, "require-dev": { - "consistence/coding-standard": "^3.0.1", - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", - "ergebnis/composer-normalize": "^2.0.2", - "jakub-onderka/php-parallel-lint": "^1.0", - "phing/phing": "^2.16.0", - "phpstan/phpstan-phpunit": "^0.12", - "phpstan/phpstan-strict-rules": "^0.12", - "phpunit/phpunit": "^7.0", - "slevomat/coding-standard": "^4.5.2" + "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": "0.12-dev" + "dev-master": "1.1-dev" }, "phpstan": { "includes": [ @@ -4416,42 +5309,41 @@ "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/0.12.2" + "source": "https://github.com/phpstan/phpstan-php-parser/tree/1.1.0" }, - "time": "2020-07-21T14:50:29+00:00" + "time": "2021-12-16T19:43:32+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "dev-master", + "version": "1.0.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "52f7072ddc5f81492f9d2de65a24813a48c90b18" + "reference": "9eb88c9f689003a8a2a5ae9e010338ee94dc39b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/52f7072ddc5f81492f9d2de65a24813a48c90b18", - "reference": "52f7072ddc5f81492f9d2de65a24813a48c90b18", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/9eb88c9f689003a8a2a5ae9e010338ee94dc39b3", + "reference": "9eb88c9f689003a8a2a5ae9e010338ee94dc39b3", "shasum": "" }, "require": { "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^0.12.86" + "phpstan/phpstan": "^1.0" }, "conflict": { "phpunit/phpunit": "<7.0" }, "require-dev": { - "phing/phing": "^2.16.3", + "nikic/php-parser": "^4.13.0", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-strict-rules": "^0.12.6", - "phpunit/phpunit": "^7.5.20" + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5" }, - "default-branch": true, "type": "phpstan-extension", "extra": { "branch-alias": { - "dev-master": "0.12-dev" + "dev-master": "1.0-dev" }, "phpstan": { "includes": [ @@ -4472,38 +5364,38 @@ "description": "PHPUnit extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/master" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.0.0" }, - "time": "2021-04-30T11:10:37+00:00" + "time": "2021-10-14T08:03:54+00:00" }, { "name": "phpstan/phpstan-strict-rules", - "version": "0.12.9", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "0705fefc7c9168529fd130e341428f5f10f4f01d" + "reference": "e12d55f74a8cca18c6e684c6450767e055ba7717" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/0705fefc7c9168529fd130e341428f5f10f4f01d", - "reference": "0705fefc7c9168529fd130e341428f5f10f4f01d", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/e12d55f74a8cca18c6e684c6450767e055ba7717", + "reference": "e12d55f74a8cca18c6e684c6450767e055ba7717", "shasum": "" }, "require": { "php": "^7.1 || ^8.0", - "phpstan/phpstan": "^0.12.66" + "phpstan/phpstan": "^1.2.0" }, "require-dev": { - "phing/phing": "^2.16.3", + "nikic/php-parser": "^4.13.0", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-phpunit": "^0.12.16", - "phpunit/phpunit": "^7.5.20" + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.5" }, "type": "phpstan-extension", "extra": { "branch-alias": { - "dev-master": "0.12-dev" + "dev-master": "1.0-dev" }, "phpstan": { "includes": [ @@ -4523,29 +5415,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/0.12.9" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.1.0" }, - "time": "2021-01-13T08:50:28+00:00" + "time": "2021-11-18T09:30:29+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.6", + "version": "9.2.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "f6293e1b30a2354e8428e004689671b83871edde" + "reference": "d5850aaf931743067f4bfc1ae4cbd06468400687" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f6293e1b30a2354e8428e004689671b83871edde", - "reference": "f6293e1b30a2354e8428e004689671b83871edde", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/d5850aaf931743067f4bfc1ae4cbd06468400687", + "reference": "d5850aaf931743067f4bfc1ae4cbd06468400687", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.10.2", + "nikic/php-parser": "^4.13.0", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -4594,7 +5486,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.6" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.10" }, "funding": [ { @@ -4602,20 +5494,20 @@ "type": "github" } ], - "time": "2021-03-28T07:26:59+00:00" + "time": "2021-12-05T09:12:13+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "3.0.5", + "version": "3.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8" + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/aa4be8575f26070b100fccb67faabb28f21f66f8", - "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", "shasum": "" }, "require": { @@ -4654,7 +5546,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.5" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" }, "funding": [ { @@ -4662,7 +5554,7 @@ "type": "github" } ], - "time": "2020-09-28T05:57:25+00:00" + "time": "2021-12-02T12:48:52+00:00" }, { "name": "phpunit/php-invoker", @@ -4847,16 +5739,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.4", + "version": "9.5.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "c73c6737305e779771147af66c96ca6a7ed8a741" + "reference": "d0dc8b6999c937616df4fb046792004b33fd31c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c73c6737305e779771147af66c96ca6a7ed8a741", - "reference": "c73c6737305e779771147af66c96ca6a7ed8a741", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d0dc8b6999c937616df4fb046792004b33fd31c5", + "reference": "d0dc8b6999c937616df4fb046792004b33fd31c5", "shasum": "" }, "require": { @@ -4886,7 +5778,7 @@ "sebastian/global-state": "^5.0.1", "sebastian/object-enumerator": "^4.0.3", "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^2.3", + "sebastian/type": "^2.3.4", "sebastian/version": "^3.0.2" }, "require-dev": { @@ -4907,11 +5799,11 @@ } }, "autoload": { - "classmap": [ - "src/" - ], "files": [ "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -4934,7 +5826,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.4" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.7" }, "funding": [ { @@ -4946,7 +5838,67 @@ "type": "github" } ], - "time": "2021-03-23T07:16:29+00:00" + "time": "2021-07-19T06:14:47+00:00" + }, + { + "name": "rector/rector", + "version": "0.12.15", + "source": { + "type": "git", + "url": "https://github.com/rectorphp/rector.git", + "reference": "e923bcd0d675dc4d8746da0554089b484044d0b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/e923bcd0d675dc4d8746da0554089b484044d0b5", + "reference": "e923bcd0d675dc4d8746da0554089b484044d0b5", + "shasum": "" + }, + "require": { + "php": "^7.1|^8.0", + "phpstan/phpstan": "^1.4.2" + }, + "conflict": { + "phpstan/phpdoc-parser": "<1.2", + "rector/rector-cakephp": "*", + "rector/rector-doctrine": "*", + "rector/rector-laravel": "*", + "rector/rector-nette": "*", + "rector/rector-phpoffice": "*", + "rector/rector-phpunit": "*", + "rector/rector-prefixed": "*", + "rector/rector-symfony": "*" + }, + "bin": [ + "bin/rector" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.12-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.12.15" + }, + "funding": [ + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2022-01-26T12:02:35+00:00" }, { "name": "sebastian/cli-parser", @@ -5377,16 +6329,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.3", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65" + "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/d89cc98761b8cb5a1a235a6b703ae50d34080e65", - "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/65e8b7db476c5dd267e65eea9cab77584d3cfff9", + "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9", "shasum": "" }, "require": { @@ -5435,14 +6387,14 @@ } ], "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "http://www.github.com/sebastianbergmann/exporter", + "homepage": "https://www.github.com/sebastianbergmann/exporter", "keywords": [ "export", "exporter" ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.3" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4" }, "funding": [ { @@ -5450,20 +6402,20 @@ "type": "github" } ], - "time": "2020-09-28T05:24:23+00:00" + "time": "2021-11-11T14:18:36+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.2", + "version": "5.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "a90ccbddffa067b51f574dea6eb25d5680839455" + "reference": "23bd5951f7ff26f12d4e3242864df3e08dec4e49" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/a90ccbddffa067b51f574dea6eb25d5680839455", - "reference": "a90ccbddffa067b51f574dea6eb25d5680839455", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/23bd5951f7ff26f12d4e3242864df3e08dec4e49", + "reference": "23bd5951f7ff26f12d4e3242864df3e08dec4e49", "shasum": "" }, "require": { @@ -5506,7 +6458,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.2" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.3" }, "funding": [ { @@ -5514,7 +6466,7 @@ "type": "github" } ], - "time": "2020-10-26T15:55:19+00:00" + "time": "2021-06-11T13:31:12+00:00" }, { "name": "sebastian/lines-of-code", @@ -5805,16 +6757,16 @@ }, { "name": "sebastian/type", - "version": "2.3.1", + "version": "2.3.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2" + "reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/81cd61ab7bbf2de744aba0ea61fae32f721df3d2", - "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b8cd8a1c753c90bc1a0f5372170e3e489136f914", + "reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914", "shasum": "" }, "require": { @@ -5849,7 +6801,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/2.3.1" + "source": "https://github.com/sebastianbergmann/type/tree/2.3.4" }, "funding": [ { @@ -5857,7 +6809,7 @@ "type": "github" } ], - "time": "2020-10-26T13:18:59+00:00" + "time": "2021-06-15T12:49:02+00:00" }, { "name": "sebastian/version", @@ -5975,147 +6927,6 @@ ], "time": "2020-11-11T09:19:24+00:00" }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.22.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "c6c942b1ac76c82448322025e084cadc56048b4e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e", - "reference": "c6c942b1ac76c82448322025e084cadc56048b4e", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.22-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.22.1" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2021-01-07T16:49:33+00:00" - }, - { - "name": "symfony/process", - "version": "v5.2.4", - "source": { - "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "313a38f09c77fbcdc1d223e57d368cea76a2fd2f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/313a38f09c77fbcdc1d223e57d368cea76a2fd2f", - "reference": "313a38f09c77fbcdc1d223e57d368cea76a2fd2f", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.15" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Executes commands in sub-processes", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/process/tree/v5.2.4" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2021-01-27T10:15:41+00:00" - }, { "name": "theseer/tokenizer", "version": "1.2.0", @@ -6393,11 +7204,11 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^7.4 || ^8.0" + "php": "^8.0" }, "platform-dev": [], "platform-overrides": { - "php": "7.4.6" + "php": "8.0.99" }, - "plugin-api-version": "2.1.0" + "plugin-api-version": "2.2.0" } diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index ea66c26407..5af3a763db 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -1,25 +1,7 @@ parameters: featureToggles: bleedingEdge: true - closureUsesThis: true - randomIntParameters: true - nullCoalesce: true - fileWhitespace: true - unusedClassElements: true - readComposerPhpVersion: true - dateTimeInstantiation: true - detectDuplicateStubFiles: true - checkLogicalAndConstantCondition: true - checkLogicalOrConstantCondition: true - checkMissingTemplateTypeInParameter: true - wrongVarUsage: true - arrayDestructuring: true - objectFromNewClass: true skipCheckGenericClasses: [] - rememberFunctionValues: true - preciseExceptionTracking: true - apiRules: true - deepInspectTypes: true - neverInGenericReturnType: true + explicitMixedInUnknownGenericNew: true stubFiles: - - ../stubs/arrayFunctions.stub + - ../stubs/bleedingEdge/Countable.stub diff --git a/conf/config.level0.neon b/conf/config.level0.neon index 5df0e46ec7..9b911c7b1f 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -1,37 +1,19 @@ parameters: customRulesetUsed: false - missingClosureNativeReturnCheckObjectTypehint: false conditionalTags: - PHPStan\Rules\Api\ApiInstantiationRule: - phpstan.rules.rule: %featureToggles.apiRules% - PHPStan\Rules\Api\ApiClassExtendsRule: - phpstan.rules.rule: %featureToggles.apiRules% - PHPStan\Rules\Api\ApiClassImplementsRule: - phpstan.rules.rule: %featureToggles.apiRules% - PHPStan\Rules\Api\ApiInterfaceExtendsRule: - phpstan.rules.rule: %featureToggles.apiRules% - PHPStan\Rules\Api\ApiMethodCallRule: - phpstan.rules.rule: %featureToggles.apiRules% - PHPStan\Rules\Api\ApiStaticCallRule: - phpstan.rules.rule: %featureToggles.apiRules% - PHPStan\Rules\Api\ApiTraitUseRule: - phpstan.rules.rule: %featureToggles.apiRules% - PHPStan\Rules\Api\PhpStanNamespaceIn3rdPartyPackageRule: - phpstan.rules.rule: %featureToggles.apiRules% - PHPStan\Rules\Functions\ClosureUsesThisRule: - phpstan.rules.rule: %featureToggles.closureUsesThis% - PHPStan\Rules\Missing\MissingClosureNativeReturnTypehintRule: - phpstan.rules.rule: %checkMissingClosureNativeReturnTypehintRule% - PHPStan\Rules\Whitespace\FileWhitespaceRule: - phpstan.rules.rule: %featureToggles.fileWhitespace% PHPStan\Rules\Properties\UninitializedPropertyRule: phpstan.rules.rule: %checkUninitializedProperties% -parametersSchema: - missingClosureNativeReturnCheckObjectTypehint: bool() - rules: + - PHPStan\Rules\Api\ApiInstantiationRule + - PHPStan\Rules\Api\ApiClassExtendsRule + - PHPStan\Rules\Api\ApiClassImplementsRule + - PHPStan\Rules\Api\ApiInterfaceExtendsRule + - PHPStan\Rules\Api\ApiMethodCallRule + - PHPStan\Rules\Api\ApiStaticCallRule + - PHPStan\Rules\Api\ApiTraitUseRule + - PHPStan\Rules\Api\PhpStanNamespaceIn3rdPartyPackageRule - PHPStan\Rules\Arrays\DuplicateKeysInLiteralArraysRule - PHPStan\Rules\Arrays\EmptyArrayItemRule - PHPStan\Rules\Arrays\OffsetAccessWithoutDimForReadingRule @@ -40,20 +22,25 @@ rules: - PHPStan\Rules\Classes\ClassConstantAttributesRule - PHPStan\Rules\Classes\ClassConstantRule - PHPStan\Rules\Classes\DuplicateDeclarationRule + - PHPStan\Rules\Classes\EnumSanityRule - PHPStan\Rules\Classes\ExistingClassesInClassImplementsRule + - PHPStan\Rules\Classes\ExistingClassesInEnumImplementsRule - PHPStan\Rules\Classes\ExistingClassesInInterfaceExtendsRule - - PHPStan\Rules\Classes\ExistingClassInClassExtendsRule - PHPStan\Rules\Classes\ExistingClassInTraitUseRule - PHPStan\Rules\Classes\InstantiationRule + - PHPStan\Rules\Classes\InstantiationCallableRule - PHPStan\Rules\Classes\InvalidPromotedPropertiesRule - PHPStan\Rules\Classes\NewStaticRule - PHPStan\Rules\Classes\NonClassAttributeClassRule - PHPStan\Rules\Classes\TraitAttributeClassRule + - PHPStan\Rules\Constants\FinalConstantRule + - PHPStan\Rules\EnumCases\EnumCaseAttributesRule - 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\ExistingClassesInClosureTypehintsRule - PHPStan\Rules\Functions\ExistingClassesInTypehintsRule @@ -64,39 +51,26 @@ rules: - PHPStan\Rules\Functions\ReturnNullsafeByRefRule - PHPStan\Rules\Keywords\ContinueBreakInLoopRule - PHPStan\Rules\Methods\AbstractMethodInNonAbstractClassRule + - PHPStan\Rules\Methods\CallMethodsRule + - PHPStan\Rules\Methods\CallStaticMethodsRule - PHPStan\Rules\Methods\ExistingClassesInTypehintsRule + - PHPStan\Rules\Methods\MethodCallableRule - PHPStan\Rules\Methods\MissingMethodImplementationRule - PHPStan\Rules\Methods\MethodAttributesRule + - PHPStan\Rules\Methods\StaticMethodCallableRule - PHPStan\Rules\Operators\InvalidAssignVarRule - PHPStan\Rules\Properties\AccessPropertiesInAssignRule - PHPStan\Rules\Properties\AccessStaticPropertiesInAssignRule - PHPStan\Rules\Properties\PropertyAttributesRule + - PHPStan\Rules\Properties\ReadOnlyPropertyRule - PHPStan\Rules\Variables\UnsetRule + - PHPStan\Rules\Whitespace\FileWhitespaceRule services: - - class: PHPStan\Rules\Api\ApiInstantiationRule - - - - class: PHPStan\Rules\Api\ApiClassExtendsRule - - - - class: PHPStan\Rules\Api\ApiClassImplementsRule - - - - class: PHPStan\Rules\Api\ApiInterfaceExtendsRule - - - - class: PHPStan\Rules\Api\ApiMethodCallRule - - - - class: PHPStan\Rules\Api\ApiStaticCallRule - - - - class: PHPStan\Rules\Api\ApiTraitUseRule - - - - class: PHPStan\Rules\Api\PhpStanNamespaceIn3rdPartyPackageRule + class: PHPStan\Rules\Classes\ExistingClassInClassExtendsRule + tags: + - phpstan.rules.rule - class: PHPStan\Rules\Classes\ExistingClassInInstanceOfRule @@ -120,23 +94,11 @@ services: checkFunctionNameCase: %checkFunctionNameCase% - - class: PHPStan\Rules\Functions\ClosureUsesThisRule - - - - class: PHPStan\Rules\Methods\CallMethodsRule - tags: - - phpstan.rules.rule + class: PHPStan\Rules\Constants\OverridingConstantRule arguments: - checkFunctionNameCase: %checkFunctionNameCase% - reportMagicMethods: %reportMagicMethods% - - - - class: PHPStan\Rules\Methods\CallStaticMethodsRule + checkPhpDocMethodSignatures: %checkPhpDocMethodSignatures% tags: - phpstan.rules.rule - arguments: - checkFunctionNameCase: %checkFunctionNameCase% - reportMagicMethods: %reportMagicMethods% - class: PHPStan\Rules\Methods\OverridingMethodRule @@ -145,11 +107,6 @@ services: tags: - phpstan.rules.rule - - - class: PHPStan\Rules\Missing\MissingClosureNativeReturnTypehintRule - arguments: - checkObjectTypehint: %missingClosureNativeReturnCheckObjectTypehint% - - class: PHPStan\Rules\Missing\MissingReturnRule arguments: @@ -199,6 +156,29 @@ services: checkClassCaseSensitivity: %checkClassCaseSensitivity% checkThisOnly: %checkThisOnly% + - + class: PHPStan\Rules\Functions\FunctionCallableRule + arguments: + checkFunctionNameCase: %checkFunctionNameCase% + reportMaybes: %reportMaybes% + tags: + - phpstan.rules.rule + + - + class: PHPStan\Rules\Properties\MissingReadOnlyPropertyAssignRule + arguments: + additionalConstructors: %additionalConstructors% + tags: + - phpstan.rules.rule + + - + class: PHPStan\Rules\Properties\OverridingPropertyRule + arguments: + checkPhpDocMethodSignatures: %checkPhpDocMethodSignatures% + reportMaybes: %reportMaybesInPropertyPhpDocTypes% + tags: + - phpstan.rules.rule + - class: PHPStan\Rules\Properties\UninitializedPropertyRule arguments: @@ -238,9 +218,6 @@ services: tags: - phpstan.rules.rule - - - class: PHPStan\Rules\Whitespace\FileWhitespaceRule - - class: PHPStan\Rules\Classes\LocalTypeAliasesRule arguments: diff --git a/conf/config.level1.neon b/conf/config.level1.neon index 2499484180..3b5f68d64a 100644 --- a/conf/config.level1.neon +++ b/conf/config.level1.neon @@ -7,17 +7,10 @@ parameters: reportMagicMethods: true reportMagicProperties: true - -conditionalTags: - PHPStan\Rules\Variables\VariableCertaintyNullCoalesceRule: - phpstan.rules.rule: %featureToggles.nullCoalesce% - rules: - PHPStan\Rules\Classes\UnusedConstructorParametersRule - PHPStan\Rules\Constants\ConstantRule - PHPStan\Rules\Functions\UnusedClosureUsesRule - - PHPStan\Rules\Variables\VariableCertaintyInIssetRule - -services: - - - class: PHPStan\Rules\Variables\VariableCertaintyNullCoalesceRule + - PHPStan\Rules\Variables\EmptyRule + - PHPStan\Rules\Variables\IssetRule + - PHPStan\Rules\Variables\NullCoalesceRule diff --git a/conf/config.level2.neon b/conf/config.level2.neon index c284232e28..becc7b5b02 100644 --- a/conf/config.level2.neon +++ b/conf/config.level2.neon @@ -11,10 +11,13 @@ rules: - PHPStan\Rules\Cast\InvalidCastRule - PHPStan\Rules\Cast\InvalidPartOfEncapsedStringRule - PHPStan\Rules\Cast\PrintRule + - PHPStan\Rules\Classes\AccessPrivateConstantThroughStaticRule - PHPStan\Rules\Comparison\UsageOfVoidMatchExpressionRule - PHPStan\Rules\Functions\IncompatibleDefaultParameterTypeRule - PHPStan\Rules\Generics\ClassAncestorsRule - PHPStan\Rules\Generics\ClassTemplateTypeRule + - PHPStan\Rules\Generics\EnumAncestorsRule + - PHPStan\Rules\Generics\EnumTemplateTypeRule - PHPStan\Rules\Generics\FunctionTemplateTypeRule - PHPStan\Rules\Generics\FunctionSignatureVarianceRule - PHPStan\Rules\Generics\InterfaceAncestorsRule @@ -23,15 +26,19 @@ rules: - PHPStan\Rules\Generics\MethodSignatureVarianceRule - PHPStan\Rules\Generics\TraitTemplateTypeRule - PHPStan\Rules\Generics\UsedTraitsRule + - PHPStan\Rules\Methods\CallPrivateMethodThroughStaticRule - PHPStan\Rules\Methods\IncompatibleDefaultParameterTypeRule - PHPStan\Rules\Operators\InvalidBinaryOperationRule - PHPStan\Rules\Operators\InvalidUnaryOperationRule - PHPStan\Rules\Operators\InvalidComparisonOperationRule + - PHPStan\Rules\PhpDoc\IncompatibleClassConstantPhpDocTypeRule - PHPStan\Rules\PhpDoc\IncompatiblePhpDocTypeRule - PHPStan\Rules\PhpDoc\IncompatiblePropertyPhpDocTypeRule - PHPStan\Rules\PhpDoc\InvalidPhpDocTagValueRule - PHPStan\Rules\PhpDoc\InvalidPHPStanDocTagRule - PHPStan\Rules\PhpDoc\InvalidThrowsPhpDocValueRule + - PHPStan\Rules\PhpDoc\WrongVariableNameInVarTagRule + - PHPStan\Rules\Properties\AccessPrivatePropertyThroughStaticRule services: - @@ -53,9 +60,3 @@ services: checkMissingVarTagTypehint: %checkMissingVarTagTypehint% tags: - phpstan.rules.rule - - - class: PHPStan\Rules\PhpDoc\WrongVariableNameInVarTagRule - arguments: - checkWrongVarUsage: %featureToggles.wrongVarUsage% - tags: - - phpstan.rules.rule diff --git a/conf/config.level3.neon b/conf/config.level3.neon index 45c6d129a0..51450cddef 100644 --- a/conf/config.level3.neon +++ b/conf/config.level3.neon @@ -2,7 +2,7 @@ includes: - config.level2.neon rules: - - PHPStan\Rules\Arrays\AppendedArrayItemTypeRule + - PHPStan\Rules\Arrays\ArrayDestructuringRule - PHPStan\Rules\Arrays\IterableInForeachRule - PHPStan\Rules\Arrays\OffsetAccessAssignmentRule - PHPStan\Rules\Arrays\OffsetAccessAssignOpRule @@ -10,56 +10,54 @@ rules: - PHPStan\Rules\Arrays\UnpackIterableInArrayRule - PHPStan\Rules\Functions\ArrowFunctionReturnTypeRule - PHPStan\Rules\Functions\ClosureReturnTypeRule + - PHPStan\Rules\Functions\ReturnTypeRule - PHPStan\Rules\Generators\YieldTypeRule - PHPStan\Rules\Methods\ReturnTypeRule - PHPStan\Rules\Properties\DefaultValueTypesAssignedToPropertiesRule + - PHPStan\Rules\Properties\ReadOnlyPropertyAssignRule + - PHPStan\Rules\Properties\ReadOnlyPropertyAssignRefRule - PHPStan\Rules\Properties\TypesAssignedToPropertiesRule - PHPStan\Rules\Variables\ThrowTypeRule - PHPStan\Rules\Variables\VariableCloningRule -conditionalTags: - PHPStan\Rules\Arrays\ArrayDestructuringRule: - phpstan.rules.rule: %featureToggles.arrayDestructuring% - parameters: checkPhpDocMethodSignatures: true services: - - class: PHPStan\Rules\Arrays\AppendedArrayKeyTypeRule + class: PHPStan\Rules\Arrays\InvalidKeyInArrayDimFetchRule arguments: - checkUnionTypes: %checkUnionTypes% + reportMaybes: %reportMaybes% tags: - phpstan.rules.rule - - class: PHPStan\Rules\Arrays\ArrayDestructuringRule - - - - class: PHPStan\Rules\Arrays\InvalidKeyInArrayDimFetchRule + class: PHPStan\Rules\Arrays\InvalidKeyInArrayItemRule arguments: reportMaybes: %reportMaybes% tags: - phpstan.rules.rule - - class: PHPStan\Rules\Arrays\InvalidKeyInArrayItemRule + class: PHPStan\Rules\Arrays\NonexistentOffsetInArrayDimFetchRule arguments: reportMaybes: %reportMaybes% tags: - phpstan.rules.rule - - class: PHPStan\Rules\Arrays\NonexistentOffsetInArrayDimFetchRule + class: PHPStan\Rules\Exceptions\ThrowsVoidFunctionWithExplicitThrowPointRule arguments: - reportMaybes: %reportMaybes% + exceptionTypeResolver: @exceptionTypeResolver + missingCheckedExceptionInThrows: %exceptions.check.missingCheckedExceptionInThrows% tags: - phpstan.rules.rule - - class: PHPStan\Rules\Functions\ReturnTypeRule + class: PHPStan\Rules\Exceptions\ThrowsVoidMethodWithExplicitThrowPointRule arguments: - functionReflector: @betterReflectionFunctionReflector + exceptionTypeResolver: @exceptionTypeResolver + missingCheckedExceptionInThrows: %exceptions.check.missingCheckedExceptionInThrows% tags: - phpstan.rules.rule diff --git a/conf/config.level4.neon b/conf/config.level4.neon index 86c6f528c0..61f32bd2f7 100644 --- a/conf/config.level4.neon +++ b/conf/config.level4.neon @@ -6,32 +6,22 @@ rules: - PHPStan\Rules\Comparison\NumberComparisonOperatorsConstantConditionRule - PHPStan\Rules\DeadCode\NoopRule - PHPStan\Rules\DeadCode\UnreachableStatementRule - - PHPStan\Rules\Exceptions\DeadCatchRule - - PHPStan\Rules\Functions\CallToFunctionStamentWithoutSideEffectsRule + - PHPStan\Rules\DeadCode\UnusedPrivateConstantRule + - PHPStan\Rules\DeadCode\UnusedPrivateMethodRule + - PHPStan\Rules\Exceptions\CatchWithUnthrownExceptionRule + - PHPStan\Rules\Exceptions\OverwrittenExitPointByFinallyRule + - PHPStan\Rules\Functions\CallToFunctionStatementWithoutSideEffectsRule - PHPStan\Rules\Methods\CallToConstructorStatementWithoutSideEffectsRule - - PHPStan\Rules\Methods\CallToMethodStamentWithoutSideEffectsRule - - PHPStan\Rules\Methods\CallToStaticMethodStamentWithoutSideEffectsRule + - PHPStan\Rules\Methods\CallToMethodStatementWithoutSideEffectsRule + - PHPStan\Rules\Methods\CallToStaticMethodStatementWithoutSideEffectsRule - PHPStan\Rules\Methods\NullsafeMethodCallRule - PHPStan\Rules\Properties\NullsafePropertyFetchRule - PHPStan\Rules\TooWideTypehints\TooWideArrowFunctionReturnTypehintRule - PHPStan\Rules\TooWideTypehints\TooWideClosureReturnTypehintRule - PHPStan\Rules\TooWideTypehints\TooWideFunctionReturnTypehintRule -conditionalTags: - PHPStan\Rules\Exceptions\CatchWithUnthrownExceptionRule: - phpstan.rules.rule: %featureToggles.preciseExceptionTracking% - PHPStan\Rules\Exceptions\OverwrittenExitPointByFinallyRule: - phpstan.rules.rule: %featureToggles.preciseExceptionTracking% - PHPStan\Rules\DeadCode\UnusedPrivateConstantRule: - phpstan.rules.rule: %featureToggles.unusedClassElements% - PHPStan\Rules\DeadCode\UnusedPrivateMethodRule: - phpstan.rules.rule: %featureToggles.unusedClassElements% - PHPStan\Rules\DeadCode\UnusedPrivatePropertyRule: - phpstan.rules.rule: %featureToggles.unusedClassElements% - PHPStan\Rules\Variables\IssetRule: - phpstan.rules.rule: %featureToggles.nullCoalesce% - PHPStan\Rules\Variables\NullCoalesceRule: - phpstan.rules.rule: %featureToggles.nullCoalesce% +parameters: + checkAdvancedIsset: true services: - @@ -46,7 +36,6 @@ services: class: PHPStan\Rules\Comparison\BooleanAndConstantConditionRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - checkLogicalAndConstantCondition: %featureToggles.checkLogicalAndConstantCondition% tags: - phpstan.rules.rule @@ -54,7 +43,6 @@ services: class: PHPStan\Rules\Comparison\BooleanOrConstantConditionRule arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% - checkLogicalOrConstantCondition: %featureToggles.checkLogicalOrConstantCondition% tags: - phpstan.rules.rule @@ -65,18 +53,21 @@ services: tags: - phpstan.rules.rule - - - class: PHPStan\Rules\DeadCode\UnusedPrivateConstantRule - - - - class: PHPStan\Rules\DeadCode\UnusedPrivateMethodRule - - class: PHPStan\Rules\DeadCode\UnusedPrivatePropertyRule arguments: alwaysWrittenTags: %propertyAlwaysWrittenTags% alwaysReadTags: %propertyAlwaysReadTags% checkUninitializedProperties: %checkUninitializedProperties% + tags: + - phpstan.rules.rule + + - + class: PHPStan\Rules\Comparison\DoWhileLoopConstantConditionRule + arguments: + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + tags: + - phpstan.rules.rule - class: PHPStan\Rules\Comparison\ElseIfConstantConditionRule @@ -152,10 +143,18 @@ services: - phpstan.rules.rule - - class: PHPStan\Rules\Exceptions\CatchWithUnthrownExceptionRule + class: PHPStan\Rules\Comparison\WhileLoopAlwaysFalseConditionRule + arguments: + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + tags: + - phpstan.rules.rule - - class: PHPStan\Rules\Exceptions\OverwrittenExitPointByFinallyRule + class: PHPStan\Rules\Comparison\WhileLoopAlwaysTrueConditionRule + arguments: + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + tags: + - phpstan.rules.rule - class: PHPStan\Rules\TooWideTypehints\TooWideMethodReturnTypehintRule @@ -163,9 +162,3 @@ services: checkProtectedAndPublicMethods: %checkTooWideReturnTypesInProtectedAndPublicMethods% tags: - phpstan.rules.rule - - - - class: PHPStan\Rules\Variables\IssetRule - - - - class: PHPStan\Rules\Variables\NullCoalesceRule diff --git a/conf/config.level5.neon b/conf/config.level5.neon index 57dc11d4a0..c890be88ec 100644 --- a/conf/config.level5.neon +++ b/conf/config.level5.neon @@ -1,21 +1,18 @@ includes: - config.level4.neon -conditionalTags: - PHPStan\Rules\Functions\RandomIntParametersRule: - phpstan.rules.rule: %featureToggles.randomIntParameters% - PHPStan\Rules\DateTimeInstantiationRule: - phpstan.rules.rule: %featureToggles.dateTimeInstantiation% - parameters: checkFunctionArgumentTypes: true checkArgumentsPassedByReference: true +rules: + - PHPStan\Rules\DateTimeInstantiationRule + - PHPStan\Rules\Functions\ImplodeFunctionRule services: - class: PHPStan\Rules\Functions\RandomIntParametersRule arguments: reportMaybes: %reportMaybes% - - - class: PHPStan\Rules\DateTimeInstantiationRule + tags: + - phpstan.rules.rule diff --git a/conf/config.level6.neon b/conf/config.level6.neon index 4fdeb19345..05f3616832 100644 --- a/conf/config.level6.neon +++ b/conf/config.level6.neon @@ -8,6 +8,7 @@ parameters: checkMissingTypehints: true rules: + - PHPStan\Rules\Constants\MissingClassConstantTypehintRule - PHPStan\Rules\Functions\MissingFunctionParameterTypehintRule - PHPStan\Rules\Functions\MissingFunctionReturnTypehintRule - PHPStan\Rules\Methods\MissingMethodParameterTypehintRule diff --git a/conf/config.level9.neon b/conf/config.level9.neon new file mode 100644 index 0000000000..efb3e30ffa --- /dev/null +++ b/conf/config.level9.neon @@ -0,0 +1,5 @@ +includes: + - config.level8.neon + +parameters: + checkExplicitMixed: true diff --git a/conf/config.levelmax.neon b/conf/config.levelmax.neon index 789cb34109..da48578fe3 100644 --- a/conf/config.levelmax.neon +++ b/conf/config.levelmax.neon @@ -1,2 +1,2 @@ includes: - - config.level8.neon + - config.level9.neon diff --git a/conf/config.neon b/conf/config.neon index 6ae19bb2db..0b86120409 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1,17 +1,17 @@ includes: - config.stubFiles.neon parameters: - bootstrap: null bootstrapFiles: - ../stubs/runtime/ReflectionUnionType.php + - ../stubs/runtime/ReflectionAttribute.php - ../stubs/runtime/Attribute.php + - ../stubs/runtime/ReflectionIntersectionType.php excludes_analyse: [] excludePaths: null - autoload_directories: [] - autoload_files: [] level: null paths: [] exceptions: + implicitThrows: true uncheckedExceptionRegexes: [] uncheckedExceptionClasses: [] checkedExceptionRegexes: [] @@ -22,30 +22,15 @@ parameters: featureToggles: bleedingEdge: false disableRuntimeReflectionProvider: false - closureUsesThis: false - randomIntParameters: false - nullCoalesce: false - fileWhitespace: false - unusedClassElements: false - readComposerPhpVersion: false - dateTimeInstantiation: false - detectDuplicateStubFiles: false - checkLogicalAndConstantCondition: false - checkLogicalOrConstantCondition: false - checkMissingTemplateTypeInParameter: false - wrongVarUsage: false - arrayDestructuring: false - objectFromNewClass: false skipCheckGenericClasses: - - RecursiveIterator - - RecursiveArrayIterator - rememberFunctionValues: false - preciseExceptionTracking: false - apiRules: false - deepInspectTypes: false - neverInGenericReturnType: false + - DatePeriod + - CallbackFilterIterator + - FilterIterator + - RecursiveCallbackFilterIterator + explicitMixedInUnknownGenericNew: false fileExtensions: - php + checkAdvancedIsset: false checkAlwaysTrueCheckTypeFunctionCall: false checkAlwaysTrueInstanceof: false checkAlwaysTrueStrictComparison: false @@ -67,28 +52,26 @@ parameters: checkPhpDocMissingReturn: false checkPhpDocMethodSignatures: false checkExtraArguments: false - checkMissingClosureNativeReturnTypehintRule: false checkMissingTypehints: false checkTooWideReturnTypesInProtectedAndPublicMethods: false checkUninitializedProperties: false inferPrivatePropertyTypeFromConstructor: false - implicitThrows: true reportMaybes: false reportMaybesInMethodSignatures: false + reportMaybesInPropertyPhpDocTypes: false reportStaticMethodSignatures: false mixinExcludeClasses: [] scanFiles: [] scanDirectories: [] parallel: jobSize: 20 - processTimeout: 60.0 + processTimeout: 600.0 maximumNumberOfProcesses: 32 minimumNumberOfJobsPerProcess: 2 buffer: 134217728 # 128 MB phpVersion: null polluteScopeWithLoopInitialAssignments: true polluteScopeWithAlwaysIterableForeach: true - polluteCatchScopeWithTryAssignments: false propertyAlwaysWrittenTags: [] propertyAlwaysReadTags: [] additionalConstructors: [] @@ -111,10 +94,28 @@ parameters: memoryLimitFile: %tmpDir%/.memory_limit tempResultCachePath: %tmpDir%/resultCaches resultCachePath: %tmpDir%/resultCache.php + resultCacheChecksProjectExtensionFilesDependencies: false staticReflectionClassNamePatterns: - - '#^PhpParser\\#' - - '#^PHPStan\\#' - - '#^Hoa\\#' + - '#^PhpParser\\#i' + - '#^PHPStan\\#i' + - '#^Hoa\\#i' + - '#^Symfony\\Polyfill\\Php80\\#i' + - '#^Symfony\\Polyfill\\Mbstring\\#i' + - '#^Symfony\\Polyfill\\Intl\\Normalizer\\#i' + - '#^Symfony\\Polyfill\\Php73\\#i' + - '#^Symfony\\Polyfill\\Php74\\#i' + - '#^Symfony\\Polyfill\\Php72\\#i' + - '#^Symfony\\Polyfill\\Intl\\Grapheme\\#i' + - '#^Composer\\#i' + - '#^ReflectionUnionType$#i' + - '#^Attribute$#i' + - '#^ReturnTypeWillChange$#i' + - '#^ReflectionIntersectionType$#i' + - '#^UnitEnum$#i' + - '#^BackedEnum$#i' + - '#^ReflectionEnum$#i' + - '#^ReflectionEnumUnitCase$#i' + - '#^ReflectionEnumBackedCase$#i' dynamicConstantNames: - ICONV_IMPL - LIBXML_VERSION @@ -125,6 +126,9 @@ parameters: - PHP_RELEASE_VERSION - PHP_VERSION_ID - PHP_EXTRA_VERSION + - PHP_WINDOWS_VERSION_MAJOR + - PHP_WINDOWS_VERSION_MINOR + - PHP_WINDOWS_VERSION_BUILD - PHP_ZTS - PHP_DEBUG - PHP_MAXPATHLEN @@ -156,15 +160,19 @@ parameters: - PHP_SHLIB_SUFFIX - PHP_FD_SETSIZE - OPENSSL_VERSION_NUMBER + - ZEND_DEBUG_BUILD + - ZEND_THREAD_SAFE + customRulesetUsed: null editorUrl: null + __validate: true extensions: rules: PHPStan\DependencyInjection\RulesExtension conditionalTags: PHPStan\DependencyInjection\ConditionalTagsExtension parametersSchema: PHPStan\DependencyInjection\ParametersSchemaExtension + validateIgnoredErrors: PHPStan\DependencyInjection\ValidateIgnoredErrorsExtension parametersSchema: - bootstrap: schema(string(), nullable()) bootstrapFiles: listOf(string()) excludes_analyse: listOf(string()) excludePaths: schema(anyOf( @@ -179,11 +187,10 @@ parametersSchema: analyseAndScan: listOf(string()) ]) ), nullable()) - autoload_directories: listOf(string()) - autoload_files: listOf(string()) level: schema(anyOf(int(), string()), nullable()) paths: listOf(string()) exceptions: structure([ + implicitThrows: bool(), uncheckedExceptionRegexes: listOf(string()), uncheckedExceptionClasses: listOf(string()), checkedExceptionRegexes: listOf(string()), @@ -196,28 +203,11 @@ parametersSchema: featureToggles: structure([ bleedingEdge: bool(), disableRuntimeReflectionProvider: bool(), - closureUsesThis: bool(), - randomIntParameters: bool(), - nullCoalesce: bool(), - fileWhitespace: bool(), - unusedClassElements: bool(), - readComposerPhpVersion: bool(), - dateTimeInstantiation: bool(), - detectDuplicateStubFiles: bool(), - checkLogicalAndConstantCondition: bool(), - checkLogicalOrConstantCondition: bool(), - checkMissingTemplateTypeInParameter: bool(), - wrongVarUsage: bool(), - arrayDestructuring: bool(), - objectFromNewClass: bool(), skipCheckGenericClasses: listOf(string()), - rememberFunctionValues: bool(), - preciseExceptionTracking: bool(), - apiRules: bool(), - deepInspectTypes: bool(), - neverInGenericReturnType: bool() + explicitMixedInUnknownGenericNew: bool(), ]) fileExtensions: listOf(string()) + checkAdvancedIsset: bool() checkAlwaysTrueCheckTypeFunctionCall: bool() checkAlwaysTrueInstanceof: bool() checkAlwaysTrueStrictComparison: bool() @@ -239,15 +229,15 @@ parametersSchema: checkPhpDocMissingReturn: bool() checkPhpDocMethodSignatures: bool() checkExtraArguments: bool() - checkMissingClosureNativeReturnTypehintRule: bool() checkMissingTypehints: bool() checkTooWideReturnTypesInProtectedAndPublicMethods: bool() checkUninitializedProperties: bool() inferPrivatePropertyTypeFromConstructor: bool() - implicitThrows: bool() + tipsOfTheDay: bool() reportMaybes: bool() reportMaybesInMethodSignatures: bool() + reportMaybesInPropertyPhpDocTypes: bool() reportStaticMethodSignatures: bool() parallel: structure([ jobSize: int(), @@ -256,10 +246,9 @@ parametersSchema: minimumNumberOfJobsPerProcess: int(), buffer: int() ]) - phpVersion: schema(anyOf(schema(int(), min(70100), max(80099))), nullable()) + phpVersion: schema(anyOf(schema(int(), min(70100), max(80199))), nullable()) polluteScopeWithLoopInitialAssignments: bool() polluteScopeWithAlwaysIterableForeach: bool() - polluteCatchScopeWithTryAssignments: bool() propertyAlwaysWrittenTags: listOf(string()) propertyAlwaysReadTags: listOf(string()) additionalConstructors: listOf(string()) @@ -299,9 +288,10 @@ parametersSchema: memoryLimitFile: string() tempResultCachePath: string() resultCachePath: string() + resultCacheChecksProjectExtensionFilesDependencies: bool() staticReflectionClassNamePatterns: listOf(string()) dynamicConstantNames: listOf(string()) - customRulesetUsed: bool() + customRulesetUsed: schema(bool(), nullable()) rootDir: string() tmpDir: string() currentWorkingDirectory: string() @@ -316,9 +306,10 @@ parametersSchema: debugMode: bool() productionMode: bool() tempDir: string() + __validate: bool() # internal parameters only for DerivativeContainerFactory - additionalConfigFiles: listOf(string()) + additionalConfigFiles: arrayOf(string()) generateBaselineFile: schema(string(), nullable()) analysedPaths: listOf(string()) composerAutoloaderProjectPaths: listOf(string()) @@ -353,6 +344,9 @@ services: - class: PhpParser\NodeVisitor\NameResolver + arguments: + options: + preserveOriginalNames: true - class: PhpParser\NodeVisitor\NodeConnectingVisitor @@ -377,7 +371,6 @@ services: class: PHPStan\Php\PhpVersionFactoryFactory arguments: versionId: %phpVersion% - readComposerPhpVersion: %featureToggles.readComposerPhpVersion% composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% - @@ -406,8 +399,6 @@ services: - class: PHPStan\PhpDoc\TypeNodeResolver - arguments: - deepInspectTypes: %featureToggles.deepInspectTypes% - class: PHPStan\PhpDoc\TypeNodeResolverExtensionRegistryProvider @@ -427,6 +418,7 @@ services: - class: PHPStan\Analyser\FileAnalyser arguments: + parser: @defaultAnalysisParser reportUnmatchedIgnoredErrors: %reportUnmatchedIgnoredErrors% - @@ -445,14 +437,13 @@ services: - class: PHPStan\Analyser\NodeScopeResolver arguments: - classReflector: @nodeScopeResolverClassReflector + parser: @defaultAnalysisParser + reflector: @nodeScopeResolverReflector polluteScopeWithLoopInitialAssignments: %polluteScopeWithLoopInitialAssignments% - polluteCatchScopeWithTryAssignments: %polluteCatchScopeWithTryAssignments% polluteScopeWithAlwaysIterableForeach: %polluteScopeWithAlwaysIterableForeach% earlyTerminatingMethodCalls: %earlyTerminatingMethodCalls% earlyTerminatingFunctionCalls: %earlyTerminatingFunctionCalls% - implicitThrows: %implicitThrows% - preciseExceptionTracking: %featureToggles.preciseExceptionTracking% + implicitThrows: %exceptions.implicitThrows% - implement: PHPStan\Analyser\ResultCache\ResultCacheManagerFactory @@ -468,6 +459,7 @@ services: bootstrapFiles: %bootstrapFiles% scanFiles: %scanFiles% scanDirectories: %scanDirectories% + checkDependenciesOfProjectExtensionFiles: %resultCacheChecksProjectExtensionFilesDependencies% - class: PHPStan\Analyser\ResultCache\ResultCacheClearer @@ -483,7 +475,6 @@ services: - class: PHPStan\Command\AnalyseApplication arguments: - memoryLimitFile: %memoryLimitFile% internalErrorsCountLimit: %internalErrorsCountLimit% - @@ -497,21 +488,13 @@ services: fixerTmpDir: %fixerTmpDir% maximumNumberOfProcesses: %parallel.maximumNumberOfProcesses% - - - class: PHPStan\Command\IgnoredRegexValidator - arguments: - parser: @regexParser - - - - class: PHPStan\Dependency\DependencyDumper - arguments: - fileFinder: @fileFinderAnalyse - - class: PHPStan\Dependency\DependencyResolver - class: PHPStan\Dependency\ExportedNodeFetcher + arguments: + parser: @defaultAnalysisParser - class: PHPStan\Dependency\ExportedNodeResolver @@ -541,6 +524,9 @@ services: analysedPathsFromConfig: %analysedPathsFromConfig% usedLevel: %usedLevel% generateBaselineFile: %generateBaselineFile% + cliAutoloadFile: %cliAutoloadFile% + singleReflectionFile: %singleReflectionFile% + singleReflectionInsteadOfFile: %singleReflectionInsteadOfFile% - class: PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider @@ -620,12 +606,6 @@ services: maximumNumberOfProcesses: %parallel.maximumNumberOfProcesses% minimumNumberOfJobsPerProcess: %parallel.minimumNumberOfJobsPerProcess% - - - class: PHPStan\Parser\CachedParser - arguments: - originalParser: @pathRoutingParser - cachedNodesByStringCountMax: %cache.nodesByStringCountMax% - - class: PHPStan\Parser\FunctionCallStatementFinder @@ -634,6 +614,8 @@ services: - implement: PHPStan\Reflection\FunctionReflectionFactory + arguments: + parser: @defaultAnalysisParser - class: PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension @@ -646,9 +628,13 @@ services: - class: PHPStan\Reflection\BetterReflection\SourceLocator\FileNodesFetcher + arguments: + parser: @defaultAnalysisParser - class: PHPStan\Reflection\BetterReflection\SourceLocator\AutoloadSourceLocator + arguments: + disableRuntimeReflectionProvider: %featureToggles.disableRuntimeReflectionProvider% - class: PHPStan\Reflection\BetterReflection\SourceLocator\ComposerJsonAndInstalledJsonSourceLocatorMaker @@ -687,11 +673,14 @@ services: - class: PHPStan\Reflection\Php\PhpClassReflectionExtension arguments: + parser: @defaultAnalysisParser inferPrivatePropertyTypeFromConstructor: %inferPrivatePropertyTypeFromConstructor% universalObjectCratesClasses: %universalObjectCratesClasses% - implement: PHPStan\Reflection\Php\PhpMethodReflectionFactory + arguments: + parser: @defaultAnalysisParser - class: PHPStan\Reflection\Php\Soap\SoapClientMethodsClassReflectionExtension @@ -712,7 +701,7 @@ services: - class: PHPStan\Reflection\SignatureMap\NativeFunctionReflectionProvider arguments: - functionReflector: @betterReflectionFunctionReflector + reflector: @betterReflectionReflector - class: PHPStan\Reflection\SignatureMap\SignatureMapParser @@ -798,18 +787,19 @@ services: checkArgumentsPassedByReference: %checkArgumentsPassedByReference% checkExtraArguments: %checkExtraArguments% checkMissingTypehints: %checkMissingTypehints% - checkNeverInGenericReturnType: %featureToggles.neverInGenericReturnType% - class: PHPStan\Rules\FunctionDefinitionCheck arguments: checkClassCaseSensitivity: %checkClassCaseSensitivity% checkThisOnly: %checkThisOnly% - checkMissingTemplateTypeInParameter: %featureToggles.checkMissingTemplateTypeInParameter% - class: PHPStan\Rules\FunctionReturnTypeCheck + - + class: PHPStan\Rules\Generics\CrossCheckInterfacesHelper + - class: PHPStan\Rules\Generics\GenericAncestorsCheck arguments: @@ -829,6 +819,21 @@ services: - class: PHPStan\Rules\IssetCheck + arguments: + checkAdvancedIsset: %checkAdvancedIsset% + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + + - + class: PHPStan\Rules\Methods\MethodCallCheck + arguments: + checkFunctionNameCase: %checkFunctionNameCase% + reportMagicMethods: %reportMagicMethods% + + - + class: PHPStan\Rules\Methods\StaticMethodCallCheck + arguments: + checkFunctionNameCase: %checkFunctionNameCase% + reportMagicMethods: %reportMagicMethods% - # checked as part of OverridingMethodRule @@ -844,7 +849,6 @@ services: checkGenericClassInNonGenericObjectType: %checkGenericClassInNonGenericObjectType% checkMissingCallableSignature: %checkMissingCallableSignature% skipCheckGenericClasses: %featureToggles.skipCheckGenericClasses% - deepInspectTypes: %featureToggles.deepInspectTypes% - class: PHPStan\Rules\NullsafeCheck @@ -854,8 +858,6 @@ services: - class: PHPStan\Rules\PhpDoc\UnresolvableTypeHelper - arguments: - deepInspectTypes: %featureToggles.deepInspectTypes% - class: PHPStan\Rules\Properties\LazyReadWritePropertiesExtensionProvider @@ -882,17 +884,29 @@ services: - class: PHPStan\Type\FileTypeMapper + arguments: + phpParser: @defaultAnalysisParser - class: PHPStan\Type\TypeAliasResolver + factory: PHPStan\Type\UsefulTypeAliasResolver arguments: globalTypeAliases: %typeAliases% + - + class: PHPStan\Type\TypeAliasResolverProvider + factory: PHPStan\Type\LazyTypeAliasResolverProvider + - class: PHPStan\Type\Php\ArgumentBasedFunctionReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\ArrayColumnFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\ArrayCombineFunctionReturnTypeExtension tags: @@ -918,6 +932,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\ArrayFlipFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\ArrayKeyDynamicReturnTypeExtension tags: @@ -953,6 +972,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\ArrayNextDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\ArrayPopFunctionReturnTypeExtension tags: @@ -983,6 +1007,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\ArraySpliceFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\ArraySearchFunctionDynamicReturnTypeExtension tags: @@ -1045,6 +1074,16 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\DateFormatFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\DateFormatMethodReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + - class: PHPStan\Type\Php\DateFunctionReturnTypeExtension tags: @@ -1095,11 +1134,6 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - - class: PHPStan\Type\Php\GetoptFunctionDynamicReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\GetParentClassDynamicFunctionReturnTypeExtension tags: @@ -1109,11 +1143,6 @@ services: class: PHPStan\Type\Php\GettimeofdayDynamicFunctionReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - - class: PHPStan\Type\Php\HashHmacFunctionsReturnTypeExtension - tags: - - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\HashFunctionsReturnTypeExtension tags: @@ -1190,6 +1219,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\PregFilterFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\PregSplitDynamicReturnTypeExtension tags: @@ -1225,6 +1259,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\MbSubstituteCharacterDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\MicrotimeFunctionReturnTypeExtension tags: @@ -1235,11 +1274,51 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\ImplodeFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\NonEmptyStringFunctionsReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\StrlenFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\StrPadFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\StrRepeatFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\SubstrDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ThrowableReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + - class: PHPStan\Type\Php\ParseUrlFunctionDynamicReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\TriggerErrorDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\VersionCompareFunctionDynamicReturnTypeExtension tags: @@ -1250,6 +1329,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\RoundFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\StrtotimeFunctionReturnTypeExtension tags: @@ -1285,6 +1369,11 @@ services: tags: - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - + class: PHPStan\Type\Php\FunctionExistsFunctionTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - class: PHPStan\Type\Php\InArrayFunctionTypeSpecifyingExtension tags: @@ -1345,6 +1434,11 @@ services: tags: - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - + class: PHPStan\Type\Php\IteratorToArrayFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\IsObjectFunctionTypeSpecifyingExtension tags: @@ -1365,6 +1459,14 @@ services: tags: - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - + class: PHPStan\Type\Php\IsAFunctionTypeSpecifyingHelper + + - + class: PHPStan\Type\Php\ArrayIsListFunctionTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - class: PHPStan\Type\Php\JsonThrowOnErrorDynamicReturnTypeExtension tags: @@ -1374,6 +1476,7 @@ services: class: PHPStan\Type\Php\TypeSpecifyingFunctionsDynamicReturnTypeExtension arguments: treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + universalObjectCratesClasses: %universalObjectCratesClasses% tags: - phpstan.broker.dynamicFunctionReturnTypeExtension @@ -1392,13 +1495,18 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\StrTokFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\SprintfFunctionDynamicReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension - - class: PHPStan\Type\Php\StrvalFunctionReturnTypeExtension + class: PHPStan\Type\Php\StrvalFamilyFunctionReturnTypeExtension tags: - phpstan.broker.dynamicFunctionReturnTypeExtension @@ -1413,6 +1521,46 @@ services: - phpstan.broker.dynamicMethodReturnTypeExtension - phpstan.broker.dynamicStaticMethodReturnTypeExtension + - + class: PHPStan\Type\Php\ReflectionGetAttributesMethodReturnTypeExtension + arguments: + className: ReflectionClass + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + + - + class: PHPStan\Type\Php\ReflectionGetAttributesMethodReturnTypeExtension + arguments: + className: ReflectionClassConstant + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + + - + class: PHPStan\Type\Php\ReflectionGetAttributesMethodReturnTypeExtension + arguments: + className: ReflectionFunctionAbstract + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + + - + class: PHPStan\Type\Php\ReflectionGetAttributesMethodReturnTypeExtension + arguments: + className: ReflectionParameter + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + + - + class: PHPStan\Type\Php\ReflectionGetAttributesMethodReturnTypeExtension + arguments: + className: ReflectionProperty + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + + - + class: PHPStan\Type\Php\DatePeriodConstructorReturnTypeExtension + tags: + - phpstan.broker.dynamicStaticMethodReturnTypeExtension + exceptionTypeResolver: class: PHPStan\Rules\Exceptions\ExceptionTypeResolver factory: @PHPStan\Rules\Exceptions\DefaultExceptionTypeResolver @@ -1464,20 +1612,33 @@ services: class: PHPStan\Parser\RichParser arguments: parser: @currentPhpVersionPhpParser + lexer: @currentPhpVersionLexer autowired: no currentPhpVersionSimpleParser: + class: PHPStan\Parser\CleaningParser + arguments: + wrappedParser: @currentPhpVersionSimpleDirectParser + autowired: no + + currentPhpVersionSimpleDirectParser: class: PHPStan\Parser\SimpleParser arguments: parser: @currentPhpVersionPhpParser autowired: no + defaultAnalysisParser: + class: PHPStan\Parser\CachedParser + arguments: + originalParser: @pathRoutingParser + cachedNodesByStringCountMax: %cache.nodesByStringCountMax% + autowired: false + phpParserDecorator: class: PHPStan\Parser\PhpParserDecorator arguments: - wrappedParser: @PHPStan\Parser\Parser - autowired: - - PhpParser\Parser + wrappedParser: @defaultAnalysisParser + autowired: false currentPhpVersionLexer: class: PhpParser\Lexer @@ -1497,6 +1658,7 @@ services: stubPhpDocProvider: class: PHPStan\PhpDoc\StubPhpDocProvider arguments: + parser: @defaultAnalysisParser stubFiles: %stubFiles% # Reflection providers @@ -1518,47 +1680,48 @@ services: factory: @PHPStan\Reflection\BetterReflection\BetterReflectionSourceLocatorFactory::create autowired: false - betterReflectionClassReflector: - class: PHPStan\Reflection\BetterReflection\Reflector\MemoizingClassReflector + originalBetterReflectionReflector: + class: PHPStan\BetterReflection\Reflector\DefaultReflector arguments: sourceLocator: @betterReflectionSourceLocator + + betterReflectionReflector: + class: PHPStan\Reflection\BetterReflection\Reflector\MemoizingReflector + arguments: + reflector: @originalBetterReflectionReflector autowired: false - nodeScopeResolverClassReflector: - factory: @betterReflectionClassReflector + # deprecated + betterReflectionClassReflector: + class: PHPStan\BetterReflection\Reflector\ClassReflector + arguments: + sourceLocator: @betterReflectionSourceLocator autowired: false + # deprecated betterReflectionFunctionReflector: - class: PHPStan\Reflection\BetterReflection\Reflector\MemoizingFunctionReflector + class: PHPStan\BetterReflection\Reflector\FunctionReflector arguments: - classReflector: @betterReflectionClassReflector sourceLocator: @betterReflectionSourceLocator autowired: false + # deprecated betterReflectionConstantReflector: - class: PHPStan\Reflection\BetterReflection\Reflector\MemoizingConstantReflector + class: PHPStan\BetterReflection\Reflector\ConstantReflector arguments: - classReflector: @betterReflectionClassReflector sourceLocator: @betterReflectionSourceLocator autowired: false + nodeScopeResolverReflector: + factory: @betterReflectionReflector + autowired: false + betterReflectionProvider: class: PHPStan\Reflection\BetterReflection\BetterReflectionProvider arguments: - classReflector: @betterReflectionClassReflector - functionReflector: @betterReflectionFunctionReflector - constantReflector: @betterReflectionConstantReflector + reflector: @betterReflectionReflector autowired: false - regexParser: - class: Hoa\Compiler\Llk\Parser - factory: Hoa\Compiler\Llk\Llk::load(@regexGrammarStream) - - regexGrammarStream: - class: Hoa\File\Read - arguments: - streamName: 'hoa://Library/Regex/Grammar.pp' - runtimeReflectionProvider: class: PHPStan\Reflection\ReflectionProvider\ClassBlacklistReflectionProvider arguments: @@ -1575,8 +1738,6 @@ services: arguments: parser: @phpParserDecorator php8Parser: @php8PhpParser - autoloadDirectories: %autoload_directories% - autoloadFiles: %autoload_files% scanFiles: %scanFiles% scanDirectories: %scanDirectories% analysedPaths: %analysedPaths% @@ -1589,10 +1750,12 @@ services: implement: PHPStan\Reflection\BetterReflection\BetterReflectionProviderFactory - - class: PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber + class: PHPStan\Reflection\BetterReflection\SourceStubber\PhpStormStubsSourceStubberFactory arguments: phpParser: @php8PhpParser - phpVersionId: %phpVersion% + + - + factory: @PHPStan\Reflection\BetterReflection\SourceStubber\PhpStormStubsSourceStubberFactory::create() autowired: - PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber @@ -1630,11 +1793,6 @@ services: errorFormatter.raw: class: PHPStan\Command\ErrorFormatter\RawErrorFormatter - errorFormatter.baselineNeon: - class: PHPStan\Command\ErrorFormatter\BaselineNeonErrorFormatter - arguments: - relativePathHelper: @simpleRelativePathHelper - errorFormatter.table: class: PHPStan\Command\ErrorFormatter\TableErrorFormatter arguments: @@ -1670,6 +1828,7 @@ services: class: PHPStan\Command\ErrorFormatter\GithubErrorFormatter arguments: relativePathHelper: @simpleRelativePathHelper + errorFormatter: @errorFormatter.table errorFormatter.teamcity: class: PHPStan\Command\ErrorFormatter\TeamcityErrorFormatter diff --git a/conf/config.stubFiles.neon b/conf/config.stubFiles.neon index 158fdc5a6f..7f8c6e81d6 100644 --- a/conf/config.stubFiles.neon +++ b/conf/config.stubFiles.neon @@ -1,14 +1,21 @@ parameters: stubFiles: + - ../stubs/ReflectionAttribute.stub - ../stubs/ReflectionClass.stub + - ../stubs/ReflectionClassConstant.stub + - ../stubs/ReflectionFunctionAbstract.stub + - ../stubs/ReflectionParameter.stub + - ../stubs/ReflectionProperty.stub - ../stubs/iterable.stub - ../stubs/ArrayObject.stub - ../stubs/WeakReference.stub - ../stubs/ext-ds.stub - ../stubs/PDOStatement.stub - - ../stubs/ReflectionFunctionAbstract.stub - ../stubs/date.stub + - ../stubs/mysqli.stub - ../stubs/zip.stub - ../stubs/dom.stub - ../stubs/spl.stub - ../stubs/SplObjectStorage.stub + - ../stubs/Exception.stub + - ../stubs/arrayFunctions.stub diff --git a/conf/config.stubValidator.neon b/conf/config.stubValidator.neon index 86e25998d0..d08129c1d2 100644 --- a/conf/config.stubValidator.neon +++ b/conf/config.stubValidator.neon @@ -5,45 +5,30 @@ parameters: checkMissingIterableValueType: true checkMissingTypehints: true checkMissingCallableSignature: false + __validate: false services: - class: PHPStan\PhpDoc\StubSourceLocatorFactory arguments: - parser: @phpParserDecorator + php8Parser: @php8PhpParser stubFiles: %stubFiles% nodeScopeResolverClassReflector: - factory: @stubClassReflector + factory: @stubReflector stubBetterReflectionProvider: class: PHPStan\Reflection\BetterReflection\BetterReflectionProvider arguments: - classReflector: @stubClassReflector - functionReflector: @stubFunctionReflector - constantReflector: @stubConstantReflector + reflector: @stubReflector autowired: false - stubClassReflector: - class: PHPStan\BetterReflection\Reflector\ClassReflector + stubReflector: + class: PHPStan\BetterReflection\Reflector\DefaultReflector arguments: sourceLocator: @stubSourceLocator autowired: false - stubFunctionReflector: - class: PHPStan\BetterReflection\Reflector\FunctionReflector - arguments: - classReflector: @stubClassReflector - sourceLocator: @stubSourceLocator - autowired: false - - stubConstantReflector: - class: PHPStan\BetterReflection\Reflector\ConstantReflector - arguments: - classReflector: @stubClassReflector - sourceLocator: @stubSourceLocator - autowired: false - stubSourceLocator: class: PHPStan\BetterReflection\SourceLocator\Type\SourceLocator factory: @PHPStan\PhpDoc\StubSourceLocatorFactory::create() diff --git a/patches/BooleanTypeMapper.patch b/patches/BooleanTypeMapper.patch new file mode 100644 index 0000000000..dfb247bb93 --- /dev/null +++ b/patches/BooleanTypeMapper.patch @@ -0,0 +1,13 @@ +@package rector/rector + +--- packages/PHPStanStaticTypeMapper/TypeMapper/BooleanTypeMapper.php 2021-12-31 13:57:22.000000000 +0100 ++++ packages/PHPStanStaticTypeMapper/TypeMapper/BooleanTypeMapper.php 2022-01-05 00:05:20.000000000 +0100 +@@ -45,7 +45,7 @@ + } + if ($type instanceof \PHPStan\Type\Constant\ConstantBooleanType) { + // cannot be parent of union +- return new \PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode('true'); ++ return new \PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode('false'); + } + return new \PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode('bool'); + } diff --git a/patches/Buffer.patch b/patches/Buffer.patch new file mode 100644 index 0000000000..9b2e2c7f86 --- /dev/null +++ b/patches/Buffer.patch @@ -0,0 +1,56 @@ +@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 @@ + * + * @return \Iterator + */ +- public function getInnerIterator() ++ public function getInnerIterator(): ?\Iterator + { + return $this->_iterator; + } +@@ -133,6 +133,7 @@ + * + * @return mixed + */ ++ #[\ReturnTypeWillChange] + public function current() + { + return $this->getBuffer()->current()[self::BUFFER_VALUE]; +@@ -143,6 +144,7 @@ + * + * @return mixed + */ ++ #[\ReturnTypeWillChange] + public function key() + { + return $this->getBuffer()->current()[self::BUFFER_KEY]; +@@ -153,7 +155,7 @@ + * + * @return void + */ +- public function next() ++ public function next(): void + { + $innerIterator = $this->getInnerIterator(); + $buffer = $this->getBuffer(); +@@ -204,7 +206,7 @@ + * + * @return void + */ +- public function rewind() ++ public function rewind(): void + { + $innerIterator = $this->getInnerIterator(); + $buffer = $this->getBuffer(); +@@ -228,7 +230,7 @@ + * + * @return bool + */ +- public function valid() ++ public function valid(): bool + { + return + $this->getBuffer()->valid() && diff --git a/patches/Lookahead.patch b/patches/Lookahead.patch new file mode 100644 index 0000000000..b6c283492c --- /dev/null +++ b/patches/Lookahead.patch @@ -0,0 +1,54 @@ +@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 @@ + * + * @return \Iterator + */ +- public function getInnerIterator() ++ public function getInnerIterator(): ?\Iterator + { + return $this->_iterator; + } +@@ -103,6 +103,7 @@ + * + * @return mixed + */ ++ #[\ReturnTypeWillChange] + public function current() + { + return $this->_current; +@@ -113,6 +114,7 @@ + * + * @return mixed + */ ++ #[\ReturnTypeWillChange] + public function key() + { + return $this->_key; +@@ -123,6 +125,7 @@ + * + * @return void + */ ++ #[\ReturnTypeWillChange] + public function next() + { + $innerIterator = $this->getInnerIterator(); +@@ -143,6 +146,7 @@ + * + * @return void + */ ++ #[\ReturnTypeWillChange] + public function rewind() + { + $out = $this->getInnerIterator()->rewind(); +@@ -156,7 +160,7 @@ + * + * @return bool + */ +- public function valid() ++ public function valid(): bool + { + return $this->_valid; + } diff --git a/patches/NameNodeMapper.patch b/patches/NameNodeMapper.patch new file mode 100644 index 0000000000..6ca3b0a748 --- /dev/null +++ b/patches/NameNodeMapper.patch @@ -0,0 +1,13 @@ +@package rector/rector + +--- packages/StaticTypeMapper/PhpParser/NameNodeMapper.php 2021-11-23 18:38:29.000000000 +0100 ++++ packages/StaticTypeMapper/PhpParser/NameNodeMapper.php 2021-12-16 23:09:30.000000000 +0100 +@@ -106,7 +106,7 @@ + } + return new \Rector\StaticTypeMapper\ValueObject\Type\ParentObjectWithoutClassType(); + } +- return new \PHPStan\Type\ThisType($classReflection); ++ return new \PHPStan\Type\ObjectType($classReflection->getName()); + } + /** + * @return \PHPStan\Type\ArrayType|\PHPStan\Type\BooleanType|\PHPStan\Type\Constant\ConstantBooleanType|\PHPStan\Type\FloatType|\PHPStan\Type\IntegerType|\PHPStan\Type\MixedType|\PHPStan\Type\StringType diff --git a/patches/Node.patch b/patches/Node.patch new file mode 100644 index 0000000000..303ab36e75 --- /dev/null +++ b/patches/Node.patch @@ -0,0 +1,48 @@ +@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 @@ + * @return \Hoa\Protocol\Protocol + * @throws \Hoa\Protocol\Exception + */ +- public function offsetSet($name, $node) ++ public function offsetSet($name, $node): void + { + if (!($node instanceof self)) { + throw new Protocol\Exception( +@@ -141,6 +141,7 @@ + * @return \Hoa\Protocol\Protocol + * @throws \Hoa\Protocol\Exception + */ ++ #[\ReturnTypeWillChange] + public function offsetGet($name) + { + if (!isset($this[$name])) { +@@ -160,7 +161,7 @@ + * @param string $name Node's name. + * @return bool + */ +- public function offsetExists($name) ++ public function offsetExists($name): bool + { + return true === array_key_exists($name, $this->_children); + } +@@ -171,7 +172,7 @@ + * @param string $name Node's name to remove. + * @return void + */ +- public function offsetUnset($name) ++ public function offsetUnset($name): void + { + unset($this->_children[$name]); + +@@ -365,7 +366,7 @@ + * + * @return \ArrayIterator + */ +- public function getIterator() ++ public function getIterator(): \Traversable + { + return new \ArrayIterator($this->_children); + } diff --git a/patches/PDO.patch b/patches/PDO.patch new file mode 100644 index 0000000000..51d125c9d0 --- /dev/null +++ b/patches/PDO.patch @@ -0,0 +1,14 @@ +@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 @@ + * @return array|false if one or more notifications is pending, returns a single row, + * with fields message and pid, otherwise FALSE. + */ +- public function pgsqlGetNotify(int $fetchMode = PDO::FETCH_DEFAULT, int $timeoutMilliseconds = 0): array|false {} ++ public function pgsqlGetNotify(int $fetchMode = 1, int $timeoutMilliseconds = 0): array|false {} + + /** + * (PHP 5 >= 5.6.0, PHP 7, PHP 8)
diff --git a/patches/Rule.patch b/patches/Rule.patch new file mode 100644 index 0000000000..2efb475fe8 --- /dev/null +++ b/patches/Rule.patch @@ -0,0 +1,16 @@ +@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 @@ + { + $this->setName($name); + $this->setChildren($children); +- $this->setNodeId($nodeId); ++ ++ if ($nodeId !== null) { ++ $this->setNodeId($nodeId); ++ } + + return; + } diff --git a/patches/SessionHandler.patch b/patches/SessionHandler.patch new file mode 100644 index 0000000000..598c201951 --- /dev/null +++ b/patches/SessionHandler.patch @@ -0,0 +1,23 @@ +@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 @@ + *

+ */ + #[TentativeType] +- public function validateId(string $id): bool; ++ public function validateId($id): bool; + + /** + * Update timestamp of a session +@@ -163,7 +163,7 @@ + * @return bool + */ + #[TentativeType] +- public function updateTimestamp(string $id, string $data): bool; ++ public function updateTimestamp($id, $data): bool; + } + + /** diff --git a/phpcs.xml b/phpcs.xml index 03130b34b2..a8579080da 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,58 +1,71 @@ + - + src tests compiler/src + compiler/tests - - + - - + + + - + + - + - + + + 10 + - + + - + + + 10 + + - + - + + + 10 src/Command/CommandHelper.php - src/Testing/TestCase.php + src/Testing/PHPStanTestCase.php tests @@ -62,6 +75,11 @@ + + + + + @@ -69,7 +87,13 @@ - + + + + + + + @@ -83,7 +107,21 @@ + + + + + + + + + + + + + + @@ -99,7 +137,41 @@ src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + tests/*/data + tests/*/Fixture tests/e2e/resultCache_1.php tests/e2e/resultCache_2.php tests/e2e/resultCache_3.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 255ba887d3..2d8e472cdb 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -10,9 +10,19 @@ parameters: count: 1 path: src/Analyser/MutatingScope.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: "#^Parameter \\#9 \\$reflection of class PHPStan\\\\Reflection\\\\ClassReflection constructor expects ReflectionClass, object given\\.$#" + count: 1 + path: src/Analyser/NodeScopeResolver.php + - message: "#^Anonymous function has an unused use \\$container\\.$#" - count: 2 + count: 1 path: src/Command/CommandHelper.php - @@ -20,6 +30,11 @@ parameters: count: 1 path: src/Command/CommandHelper.php + - + message: "#^Static property PHPStan\\\\Command\\\\CommandHelper\\:\\:\\$reservedMemory is never read, only written\\.$#" + count: 1 + path: src/Command/CommandHelper.php + - message: "#^Parameter \\#1 \\$headers \\(array\\\\) of method PHPStan\\\\Command\\\\ErrorsConsoleStyle\\:\\:table\\(\\) should be contravariant with parameter \\$headers \\(array\\) of method Symfony\\\\Component\\\\Console\\\\Style\\\\StyleInterface\\:\\:table\\(\\)$#" count: 1 @@ -46,7 +61,7 @@ parameters: path: src/Command/FixerApplication.php - - message: "#^Call to an undefined method React\\\\Promise\\\\PromiseInterface\\\\:\\:done\\(\\)\\.$#" + message: "#^Call to an undefined method React\\\\Promise\\\\PromiseInterface\\\\:\\:done\\(\\)\\.$#" count: 1 path: src/Command/FixerApplication.php @@ -90,6 +105,14 @@ parameters: count: 1 path: src/PhpDoc/ResolvedPhpDocBlock.php + - + message: """ + #^Call to deprecated method getInstance\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: + Use PHPStan\\\\Reflection\\\\ReflectionProviderStaticAccessor instead$# + """ + count: 1 + path: src/PhpDoc/StubValidator.php + - message: "#^Return type \\(PHPStan\\\\PhpDoc\\\\Tag\\\\ParamTag\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\ParamTag\\:\\:withType\\(\\) should be covariant with return type \\(static\\(PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\)\\) of method PHPStan\\\\PhpDoc\\\\Tag\\\\TypedTag\\:\\:withType\\(\\)$#" count: 1 @@ -116,44 +139,39 @@ parameters: path: src/Reflection/BetterReflection/BetterReflectionProvider.php - - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" + message: "#^Parameter \\#9 \\$reflection of class PHPStan\\\\Reflection\\\\ClassReflection constructor expects ReflectionClass, object given\\.$#" count: 1 - path: src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php + path: src/Reflection/BetterReflection/BetterReflectionProvider.php - - message: "#^Only booleans are allowed in a negated boolean, int\\|false given\\.$#" - count: 1 - path: src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.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: 2 + path: src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php - - message: "#^Only booleans are allowed in an if condition, int\\|false given\\.$#" + message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" count: 1 path: src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php - - message: "#^Parameter \\#1 \\$haystack of function strrpos expects string, string\\|null given\\.$#" + 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\\|PhpParser\\\\Node\\\\Stmt\\\\Function_ given\\.$#" count: 1 path: src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php - - message: "#^Parameter \\#1 \\$string of function substr expects string, string\\|null given\\.$#" - count: 3 - path: src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php - - - - message: "#^Parameter \\#2 \\$subject of function preg_match expects string, string\\|null given\\.$#" + 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/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php + path: src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php - - message: "#^Parameter \\#2 \\$subject of function preg_match_all expects string, string\\|null given\\.$#" + message: "#^Only booleans are allowed in &&, int\\|false given on the right side\\.$#" count: 1 - path: src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php + path: src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php - - message: "#^Parameter \\#3 \\$subject of function preg_replace expects array\\|string, string\\|null given\\.$#" - count: 4 - path: src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php + message: "#^Only booleans are allowed in an if condition, int\\|false given\\.$#" + count: 1 + path: src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php - message: "#^Method PHPStan\\\\Reflection\\\\ClassReflection\\:\\:__construct\\(\\) has parameter \\$reflection with generic class ReflectionClass but does not specify its types\\: T$#" @@ -246,7 +264,7 @@ parameters: path: src/Rules/Api/ApiTraitUseRule.php - - message: "#^Binary operation \"\\+\" between array\\(class\\-string\\\\) and array\\\\|false results in an error\\.$#" + message: "#^Binary operation \"\\+\" between array\\{class\\-string\\\\} and array\\\\|false results in an error\\.$#" count: 1 path: src/Rules/Registry.php @@ -268,7 +286,23 @@ parameters: - message: "#^Anonymous function has an unused use \\$container\\.$#" count: 1 - path: src/Testing/TestCase.php + path: src/Testing/PHPStanTestCase.php + + - + message: """ + #^Call to deprecated method getInstance\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: + Use PHPStan\\\\Reflection\\\\ReflectionProviderStaticAccessor instead$# + """ + count: 1 + path: src/Type/ObjectType.php + + - + message: """ + #^Call to deprecated method getUniversalObjectCratesClasses\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: + Inject %%universalObjectCratesClasses%% parameter instead\\.$# + """ + count: 1 + path: src/Type/ObjectType.php - message: "#^Unreachable statement \\- code above always terminates\\.$#" @@ -305,6 +339,30 @@ parameters: count: 1 path: tests/PHPStan/Analyser/EvaluationOrderTest.php + - + message: """ + #^Call to deprecated method getClass\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: + Use PHPStan\\\\Reflection\\\\ReflectionProvider instead$# + """ + count: 1 + path: tests/PHPStan/Broker/BrokerTest.php + + - + message: """ + #^Call to deprecated method getFunction\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: + Use PHPStan\\\\Reflection\\\\ReflectionProvider instead$# + """ + count: 1 + path: tests/PHPStan/Broker/BrokerTest.php + + - + message: """ + #^Call to deprecated method hasClass\\(\\) of class PHPStan\\\\Broker\\\\Broker\\: + Use PHPStan\\\\Reflection\\\\ReflectionProvider instead$# + """ + count: 1 + path: tests/PHPStan/Broker/BrokerTest.php + - message: "#^Constant SOME_CONSTANT_IN_AUTOLOAD_FILE not found\\.$#" count: 1 @@ -320,3 +378,35 @@ parameters: count: 1 path: tests/PHPStan/Node/FileNodeTest.php + - + message: """ + #^Instantiation of deprecated class PHPStan\\\\Rules\\\\Arrays\\\\AppendedArrayItemTypeRule\\: + Replaced by PHPStan\\\\Rules\\\\Properties\\\\TypesAssignedToPropertiesRule$# + """ + count: 1 + path: tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php + + - + message: """ + #^Return type of method PHPStan\\\\Rules\\\\Arrays\\\\AppendedArrayItemTypeRuleTest\\:\\:getRule\\(\\) has typehint with deprecated class PHPStan\\\\Rules\\\\Arrays\\\\AppendedArrayItemTypeRule\\: + Replaced by PHPStan\\\\Rules\\\\Properties\\\\TypesAssignedToPropertiesRule$# + """ + count: 1 + path: tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php + + - + message: """ + #^Instantiation of deprecated class PHPStan\\\\Rules\\\\Arrays\\\\AppendedArrayKeyTypeRule\\: + Replaced by PHPStan\\\\Rules\\\\Properties\\\\TypesAssignedToPropertiesRule$# + """ + count: 1 + path: tests/PHPStan/Rules/Arrays/AppendedArrayKeyTypeRuleTest.php + + - + message: """ + #^Return type of method PHPStan\\\\Rules\\\\Arrays\\\\AppendedArrayKeyTypeRuleTest\\:\\:getRule\\(\\) has typehint with deprecated class PHPStan\\\\Rules\\\\Arrays\\\\AppendedArrayKeyTypeRule\\: + Replaced by PHPStan\\\\Rules\\\\Properties\\\\TypesAssignedToPropertiesRule$# + """ + count: 1 + path: tests/PHPStan/Rules/Arrays/AppendedArrayKeyTypeRuleTest.php + diff --git a/resources/functionMap.php b/resources/functionMap.php index 0472a4206c..215649b625 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -57,10 +57,9 @@ return [ '_' => ['string', 'message'=>'string'], -'__halt_compiler' => ['void'], -'abs' => ['int', 'number'=>'int'], +'abs' => ['0|positive-int', 'number'=>'int'], 'abs\'1' => ['float', 'number'=>'float'], -'abs\'2' => ['float|int', 'number'=>'string'], +'abs\'2' => ['float|0|positive-int', 'number'=>'string'], 'accelerator_get_configuration' => ['array'], 'accelerator_get_scripts' => ['array'], 'accelerator_get_status' => ['array', 'fetch_scripts'=>'bool'], @@ -250,7 +249,7 @@ 'apd_set_session_trace' => ['void', 'debug_level'=>'int', 'dump_directory='=>'string'], 'apd_set_session_trace_socket' => ['bool', 'tcp_server'=>'string', 'socket_type'=>'int', 'port'=>'int', 'debug_level'=>'int'], 'AppendIterator::__construct' => ['void'], -'AppendIterator::append' => ['void', 'iterator'=>'iterator'], +'AppendIterator::append' => ['void', 'iterator'=>'Iterator'], 'AppendIterator::current' => ['mixed'], 'AppendIterator::getArrayIterator' => ['ArrayIterator'], 'AppendIterator::getInnerIterator' => ['iterator'], @@ -260,10 +259,10 @@ 'AppendIterator::rewind' => ['void'], 'AppendIterator::valid' => ['bool'], 'array_change_key_case' => ['array', 'input'=>'array', 'case='=>'int'], -'array_chunk' => ['array[]', 'input'=>'array', 'size'=>'int', 'preserve_keys='=>'bool'], +'array_chunk' => ['array[]', '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' => ['int[]', 'input'=>'array'], +'array_count_values' => ['array', 'input'=>'array'], 'array_diff' => ['array', 'arr1'=>'array', 'arr2'=>'array', '...args='=>'array'], 'array_diff_assoc' => ['array', 'arr1'=>'array', 'arr2'=>'array', '...args='=>'array'], 'array_diff_key' => ['array', 'arr1'=>'array', 'arr2'=>'array', '...args='=>'array'], @@ -286,7 +285,7 @@ 'array_key_first' => ['int|string|null', 'array' => 'array'], 'array_key_last' => ['int|string|null', 'array' => 'array'], 'array_keys' => ['array', 'input'=>'array', 'search_value='=>'mixed', 'strict='=>'bool'], -'array_map' => ['array', 'callback'=>'?callable', 'input1'=>'array', '...args='=>'array'], +'array_map' => ['array', 'callback'=>'?callable', 'array'=>'array', '...args='=>'array'], 'array_merge' => ['array', 'arr1'=>'array', '...args='=>'array'], 'array_merge_recursive' => ['array', 'arr1'=>'array', '...args='=>'array'], 'array_multisort' => ['bool', '&rw_array1'=>'array', 'array1_sort_order='=>'array|int', 'array1_sort_flags='=>'array|int', '...args='=>'array|int'], @@ -303,7 +302,7 @@ 'array_search' => ['int|string|false', 'needle'=>'mixed', 'haystack'=>'array', 'strict='=>'bool'], 'array_shift' => ['mixed', '&rw_stack'=>'array'], 'array_slice' => ['array', 'input'=>'array', 'offset'=>'int', 'length='=>'?int', 'preserve_keys='=>'bool'], -'array_splice' => ['array', '&rw_input'=>'array', 'offset'=>'int', 'length='=>'int', 'replacement='=>'array|string'], +'array_splice' => ['array', '&rw_input'=>'array', 'offset'=>'int', 'length='=>'int', 'replacement='=>'mixed'], 'array_sum' => ['int|float', 'input'=>'array'], 'array_udiff' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'data_comp_func'=>'callable(mixed,mixed):int'], 'array_udiff\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', '...rest='=>'array|callable(mixed,mixed):int'], @@ -318,10 +317,10 @@ 'array_uintersect_uassoc' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'data_compare_func'=>'callable(mixed,mixed):int', 'key_compare_func'=>'callable(mixed,mixed):int'], 'array_uintersect_uassoc\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', 'arg5'=>'array|callable(mixed,mixed):int', '...rest='=>'array|callable(mixed,mixed):int'], 'array_unique' => ['array', 'array'=>'array', 'flags='=>'int'], -'array_unshift' => ['int', '&rw_stack'=>'array', 'var'=>'mixed', '...vars='=>'mixed'], +'array_unshift' => ['positive-int', '&rw_stack'=>'array', 'var'=>'mixed', '...vars='=>'mixed'], 'array_values' => ['array', 'input'=>'array'], -'array_walk' => ['bool', '&rw_input'=>'array', 'callback'=>'callable', 'userdata='=>'mixed'], -'array_walk_recursive' => ['bool', '&rw_input'=>'array', 'callback'=>'callable', 'userdata='=>'mixed'], +'array_walk' => ['bool', '&rw_input'=>'array|object', 'callback'=>'callable', 'userdata='=>'mixed'], +'array_walk_recursive' => ['bool', '&rw_input'=>'array|object', 'callback'=>'callable', 'userdata='=>'mixed'], 'ArrayAccess::offsetExists' => ['bool', 'offset'=>'mixed'], 'ArrayAccess::offsetGet' => ['mixed', 'offset'=>'mixed'], 'ArrayAccess::offsetSet' => ['void', 'offset'=>'mixed', 'value'=>'mixed'], @@ -346,8 +345,8 @@ 'ArrayIterator::seek' => ['void', 'position'=>'int'], 'ArrayIterator::serialize' => ['string'], 'ArrayIterator::setFlags' => ['void', 'flags'=>'string'], -'ArrayIterator::uasort' => ['void', 'cmp_function'=>'callable(mixed,mixed):int'], -'ArrayIterator::uksort' => ['void', 'cmp_function'=>'callable(array-key,array-key):int'], +'ArrayIterator::uasort' => ['void', 'callback'=>'callable(mixed,mixed):int'], +'ArrayIterator::uksort' => ['void', 'callback'=>'callable(array-key,array-key):int'], 'ArrayIterator::unserialize' => ['void', 'serialized'=>'string'], 'ArrayIterator::valid' => ['bool'], 'ArrayObject::__construct' => ['void', 'input='=>'array|object', 'flags='=>'int', 'iterator_class='=>'string'], @@ -369,8 +368,8 @@ 'ArrayObject::serialize' => ['string'], 'ArrayObject::setFlags' => ['void', 'flags'=>'int'], 'ArrayObject::setIteratorClass' => ['void', 'iterator_class'=>'string'], -'ArrayObject::uasort' => ['void', 'cmp_function'=>'callable'], -'ArrayObject::uksort' => ['void', 'cmp_function'=>'callable(array-key,array-key):int'], +'ArrayObject::uasort' => ['void', 'callback'=>'callable'], +'ArrayObject::uksort' => ['void', 'callback'=>'callable(array-key,array-key):int'], 'ArrayObject::unserialize' => ['void', 'serialized'=>'string'], 'arsort' => ['bool', '&rw_array_arg'=>'array', 'sort_flags='=>'int'], 'asin' => ['float', 'number'=>'float'], @@ -396,7 +395,7 @@ 'BadFunctionCallException::getLine' => ['int'], 'BadFunctionCallException::getMessage' => ['string'], 'BadFunctionCallException::getPrevious' => ['(?Throwable)|(?BadFunctionCallException)'], -'BadFunctionCallException::getTrace' => ['array'], +'BadFunctionCallException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'BadFunctionCallException::getTraceAsString' => ['string'], 'BadMethodCallException::__clone' => ['void'], 'BadMethodCallException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Throwable)|(?BadMethodCallException)'], @@ -406,7 +405,7 @@ 'BadMethodCallException::getLine' => ['int'], 'BadMethodCallException::getMessage' => ['string'], 'BadMethodCallException::getPrevious' => ['(?Throwable)|(?BadMethodCallException)'], -'BadMethodCallException::getTrace' => ['array'], +'BadMethodCallException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'BadMethodCallException::getTraceAsString' => ['string'], 'base64_decode' => ['string|false', 'str'=>'string', 'strict='=>'bool'], 'base64_encode' => ['string', 'str'=>'string'], @@ -492,7 +491,7 @@ 'bzopen' => ['resource|false', 'file'=>'string|resource', 'mode'=>'string'], 'bzread' => ['string|false', 'bz'=>'resource', 'length='=>'int'], 'bzwrite' => ['int|false', 'bz'=>'resource', 'data'=>'string', 'length='=>'int'], -'CachingIterator::__construct' => ['void', 'iterator'=>'iterator', 'flags='=>''], +'CachingIterator::__construct' => ['void', 'iterator'=>'Iterator', 'flags='=>''], 'CachingIterator::__toString' => ['string'], 'CachingIterator::count' => ['0|positive-int'], 'CachingIterator::current' => ['mixed'], @@ -925,15 +924,15 @@ 'call_user_func_array' => ['mixed', 'function'=>'callable', 'parameters'=>'array'], 'call_user_method' => ['mixed', 'method_name'=>'string', 'obj'=>'object', 'parameter='=>'mixed', '...args='=>'mixed'], 'call_user_method_array' => ['mixed', 'method_name'=>'string', 'obj'=>'object', 'params'=>'array'], -'CallbackFilterIterator::__construct' => ['void', 'iterator'=>'iterator', 'func'=>'callable'], +'CallbackFilterIterator::__construct' => ['void', 'iterator'=>'Iterator', 'func'=>'callable'], 'CallbackFilterIterator::accept' => ['bool'], 'CallbackFilterIterator::current' => ['mixed'], -'CallbackFilterIterator::getInnerIterator' => ['iterator'], +'CallbackFilterIterator::getInnerIterator' => ['Iterator'], 'CallbackFilterIterator::key' => ['mixed'], 'CallbackFilterIterator::next' => ['void'], 'CallbackFilterIterator::rewind' => ['void'], 'CallbackFilterIterator::valid' => ['bool'], -'ceil' => ['float', 'number'=>'float'], +'ceil' => ['float|false', 'number'=>'float'], 'chdb::__construct' => ['void', 'pathname'=>'string'], 'chdb::get' => ['string', 'key'=>'string'], 'chdb_create' => ['bool', 'pathname'=>'string', 'data'=>'array'], @@ -944,9 +943,9 @@ 'chmod' => ['bool', 'filename'=>'string', 'mode'=>'int'], 'chop' => ['string', 'str'=>'string', 'character_mask='=>'string'], 'chown' => ['bool', 'filename'=>'string', 'user'=>'string|int'], -'chr' => ['string', 'ascii'=>'int'], +'chr' => ['non-empty-string', 'ascii'=>'int'], 'chroot' => ['bool', 'directory'=>'string'], -'chunk_split' => ['string', 'str'=>'string', 'chunklen='=>'int', 'ending='=>'string'], +'chunk_split' => ['string', 'str'=>'string', 'chunklen='=>'positive-int', 'ending='=>'string'], 'class_alias' => ['bool', 'user_class_name'=>'string', 'alias_name'=>'string', 'autoload='=>'bool'], 'class_exists' => ['bool', 'classname'=>'string', 'autoload='=>'bool'], 'class_implements' => ['array|false', 'what'=>'object|string', 'autoload='=>'bool'], @@ -990,7 +989,7 @@ 'ClosedGeneratorException::getLine' => ['int'], 'ClosedGeneratorException::getMessage' => ['string'], 'ClosedGeneratorException::getPrevious' => ['Throwable|ClosedGeneratorException|null'], -'ClosedGeneratorException::getTrace' => ['array'], +'ClosedGeneratorException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'ClosedGeneratorException::getTraceAsString' => ['string'], 'closedir' => ['void', 'dir_handle='=>'resource'], 'closelog' => ['bool'], @@ -1060,7 +1059,7 @@ 'componere\cast' => ['Type', 'arg1'=>'', 'object'=>''], 'componere\cast_by_ref' => ['Type', 'arg1'=>'', 'object'=>''], 'confirm_pdo_ibm_compiled' => [''], -'connection_aborted' => ['int'], +'connection_aborted' => ['0|1'], 'connection_status' => ['int'], 'connection_timeout' => ['int'], 'constant' => ['mixed', 'const_name'=>'string'], @@ -1366,7 +1365,7 @@ 'Couchbase\zlibCompress' => ['string', 'data'=>'string'], 'Couchbase\zlibDecompress' => ['string', 'data'=>'string'], 'count' => ['0|positive-int', 'var'=>'Countable|array', 'mode='=>'int'], -'count_chars' => ['mixed', 'input'=>'string', 'mode='=>'int'], +'count_chars' => ['mixed', 'input'=>'string', 'mode='=>'0|1|2|3|4'], 'Countable::count' => ['0|positive-int'], 'crack_check' => ['bool', 'dictionary'=>'', 'password'=>'string'], 'crack_closedict' => ['bool', 'dictionary='=>'resource'], @@ -1525,7 +1524,7 @@ 'CURLFile::getPostFilename' => ['string'], 'CURLFile::setMimeType' => ['void', 'mime'=>'string'], 'CURLFile::setPostFilename' => ['void', 'name'=>'string'], -'current' => ['mixed', 'array_arg'=>'array'], +'current' => ['mixed', 'array_arg'=>'array|object'], 'cyrus_authenticate' => ['void', 'connection'=>'resource', 'mechlist='=>'string', 'service='=>'string', 'user='=>'string', 'minssf='=>'int', 'maxssf='=>'int', 'authname='=>'string', 'password='=>'string'], 'cyrus_bind' => ['bool', 'connection'=>'resource', 'callbacks'=>'array'], 'cyrus_close' => ['bool', 'connection'=>'resource'], @@ -1549,8 +1548,8 @@ 'date_isodate_set' => ['DateTime|false', 'object'=>'DateTime', 'year'=>'int', 'week'=>'int', 'day='=>'int|mixed'], 'date_modify' => ['DateTime|false', 'object'=>'DateTime', 'modify'=>'string'], 'date_offset_get' => ['int', 'obj'=>'DateTimeInterface'], -'date_parse' => ['array|false', 'date'=>'string'], -'date_parse_from_format' => ['array', 'format'=>'string', 'date'=>'string'], +'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_sunrise' => ['mixed', 'time'=>'int', 'format='=>'int', 'latitude='=>'float', 'longitude='=>'float', 'zenith='=>'float', 'gmt_offset='=>'float'], @@ -1591,7 +1590,7 @@ 'DatePeriod::__construct\'2' => ['void', 'iso'=>'string', 'options='=>'int'], 'DatePeriod::__wakeup' => ['void'], 'DatePeriod::getDateInterval' => ['DateInterval'], -'DatePeriod::getEndDate' => ['DateTimeInterface'], +'DatePeriod::getEndDate' => ['?DateTimeInterface'], 'DatePeriod::getStartDate' => ['DateTimeInterface'], 'DateTime::__construct' => ['void', 'time='=>'string', 'timezone='=>'?DateTimeZone'], 'DateTime::__set_state' => ['static', 'array'=>'array'], @@ -1639,12 +1638,12 @@ 'DateTimeZone::__construct' => ['void', 'timezone'=>'string'], 'DateTimeZone::__set_state' => ['DateTimeZone', 'array'=>'array'], 'DateTimeZone::__wakeup' => ['void'], -'DateTimeZone::getLocation' => ['array'], +'DateTimeZone::getLocation' => ['array{country_code: string, latitude: float, longitude: float, comments: string}|false'], 'DateTimeZone::getName' => ['string'], 'DateTimeZone::getOffset' => ['int', 'datetime'=>'DateTimeInterface'], 'DateTimeZone::getTransitions' => ['array', 'timestamp_begin='=>'int', 'timestamp_end='=>'int'], 'DateTimeZone::listAbbreviations' => ['array'], -'DateTimeZone::listIdentifiers' => ['array', 'what='=>'int', 'country='=>'string'], +'DateTimeZone::listIdentifiers' => ['array', 'what='=>'int', 'country='=>'string'], 'db2_autocommit' => ['mixed', 'connection'=>'resource', 'value='=>'int'], 'db2_bind_param' => ['bool', 'stmt'=>'resource', 'parameter_number'=>'int', 'variable_name'=>'string', 'parameter_type='=>'int', 'data_type='=>'int', 'precision='=>'int', 'scale='=>'int'], 'db2_client_info' => ['object|false', 'connection'=>'resource'], @@ -1787,7 +1786,7 @@ 'dcgettext' => ['string', 'domain_name'=>'string', 'msgid'=>'string', 'category'=>'int'], 'dcngettext' => ['string', 'domain'=>'string', 'msgid1'=>'string', 'msgid2'=>'string', 'n'=>'int', 'category'=>'int'], 'deaggregate' => ['', 'object'=>'object', 'class_name='=>'string'], -'debug_backtrace' => ['array', 'options='=>'int|bool', 'limit='=>'int'], +'debug_backtrace' => ['list', 'options='=>'int|bool', 'limit='=>'int'], 'debug_print_backtrace' => ['void', 'options='=>'int|bool', 'limit='=>'int'], 'debug_zval_dump' => ['void', '...var'=>'mixed'], 'debugger_connect' => [''], @@ -1855,7 +1854,7 @@ 'DirectoryIterator::setFileClass' => ['void', 'class_name='=>'string'], 'DirectoryIterator::setInfoClass' => ['void', 'class_name='=>'string'], 'DirectoryIterator::valid' => ['bool'], -'dirname' => ['string', 'path'=>'string', 'levels='=>'int'], +'dirname' => ['string', 'path'=>'string', 'levels='=>'positive-int'], 'disk_free_space' => ['float|false', 'path'=>'string'], 'disk_total_space' => ['float|false', 'path'=>'string'], 'diskfreespace' => ['float|false', 'path'=>'string'], @@ -1884,7 +1883,7 @@ 'DomainException::getLine' => ['int'], 'DomainException::getMessage' => ['string'], 'DomainException::getPrevious' => ['Throwable|DomainException|null'], -'DomainException::getTrace' => ['array'], +'DomainException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'DomainException::getTraceAsString' => ['string'], 'DOMAttr::__construct' => ['void', 'name'=>'string', 'value='=>'string'], 'DOMAttr::isId' => ['bool'], @@ -1922,7 +1921,7 @@ 'DOMDocument::registerNodeClass' => ['bool', 'baseclass'=>'string', 'extendedclass'=>'string'], 'DOMDocument::relaxNGValidate' => ['bool', 'filename'=>'string'], 'DOMDocument::relaxNGValidateSource' => ['bool', 'source'=>'string'], -'DOMDocument::save' => ['int', 'filename'=>'string', 'options='=>'int'], +'DOMDocument::save' => ['int|false', 'filename'=>'string', 'options='=>'int'], 'DOMDocument::saveHTML' => ['string|false', 'node='=>'?DOMNode'], 'DOMDocument::saveHTMLFile' => ['int|false', 'filename'=>'string'], 'DOMDocument::saveXML' => ['string|false', 'node='=>'?DOMNode', 'options='=>'int'], @@ -2172,6 +2171,7 @@ 'Ds\Set::join' => ['string', 'glue='=>'string'], 'Ds\Set::jsonSerialize' => ['array'], 'Ds\Set::last' => ['mixed'], +'Ds\Set::map' => ['Ds\Set', 'callback='=>'callable'], 'Ds\Set::merge' => ['Ds\Set', 'values'=>'mixed'], 'Ds\Set::reduce' => ['mixed', 'callback'=>'callable', 'initial='=>'mixed'], 'Ds\Set::remove' => ['void', '...values='=>'mixed'], @@ -2233,7 +2233,6 @@ 'each' => ['array', '&rw_arr'=>'array'], 'easter_date' => ['int', 'year='=>'int'], 'easter_days' => ['int', 'year='=>'int', 'method='=>'int'], -'echo' => ['void', 'arg1'=>'string', '...args='=>'string'], 'eio_busy' => ['resource', 'delay'=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], 'eio_cancel' => ['void', 'req'=>'resource'], 'eio_chmod' => ['resource', 'path'=>'string', 'mode'=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], @@ -2293,7 +2292,6 @@ 'eio_unlink' => ['resource', 'path'=>'string', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], 'eio_utime' => ['resource', 'path'=>'string', 'atime'=>'float', 'mtime'=>'float', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], 'eio_write' => ['resource', 'fd'=>'mixed', 'str'=>'string', 'length='=>'int', 'offset='=>'int', 'pri='=>'int', 'callback='=>'callable', 'data='=>'mixed'], -'empty' => ['bool', 'var'=>'mixed'], 'EmptyIterator::current' => ['mixed'], 'EmptyIterator::key' => ['mixed'], 'EmptyIterator::next' => ['void'], @@ -2333,7 +2331,7 @@ 'Error::getLine' => ['int'], 'Error::getMessage' => ['string'], 'Error::getPrevious' => ['Throwable|Error|null'], -'Error::getTrace' => ['array'], +'Error::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'Error::getTraceAsString' => ['string'], 'error_clear_last' => ['void'], 'error_get_last' => ['?array{type:int,message:string,file:string,line:int}'], @@ -2348,7 +2346,7 @@ 'ErrorException::getMessage' => ['string'], 'ErrorException::getPrevious' => ['Throwable|ErrorException|null'], 'ErrorException::getSeverity' => ['int'], -'ErrorException::getTrace' => ['array'], +'ErrorException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'ErrorException::getTraceAsString' => ['string'], 'escapeshellarg' => ['string', 'arg'=>'string'], 'escapeshellcmd' => ['string', 'command'=>'string'], @@ -2369,7 +2367,6 @@ 'Ev::suspend' => ['void'], 'Ev::time' => ['float'], 'Ev::verify' => ['void'], -'eval' => ['mixed', 'code_str'=>'string'], 'EvCheck::__construct' => ['void', 'callback'=>'callable', 'data='=>'mixed', 'priority='=>'int'], 'EvCheck::createStopped' => ['object', 'callback'=>'string', 'data='=>'string', 'priority='=>'string'], 'EvChild::__construct' => ['void', 'pid'=>'int', 'trace'=>'bool', 'callback'=>'callable', 'data='=>'mixed', 'priority='=>'int'], @@ -2628,18 +2625,17 @@ 'Exception::getLine' => ['int'], 'Exception::getMessage' => ['string'], 'Exception::getPrevious' => ['(?Throwable)|(?Exception)'], -'Exception::getTrace' => ['array'], +'Exception::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'Exception::getTraceAsString' => ['string'], 'exec' => ['string', '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'], 'exif_thumbnail' => ['string|false', 'filename'=>'string', '&w_width='=>'int', '&w_height='=>'int', '&w_imagetype='=>'int'], -'exit' => ['', 'status'=>'string|int'], 'exp' => ['float', 'number'=>'float'], 'expect_expectl' => ['int', 'expect'=>'resource', 'cases'=>'array', 'match='=>'array'], 'expect_popen' => ['resource|false', 'command'=>'string'], -'explode' => ['array|false', 'separator'=>'string', 'str'=>'string', 'limit='=>'int'], +'explode' => ['non-empty-array|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'], @@ -2937,13 +2933,13 @@ 'ffmpeg_movie::hasAudio' => ['bool'], 'ffmpeg_movie::hasVideo' => ['bool'], 'fgetc' => ['string|false', 'fp'=>'resource'], -'fgetcsv' => ['(?array)|(?false)', 'fp'=>'resource', 'length='=>'int', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], -'fgets' => ['string|false', 'fp'=>'resource', 'length='=>'int'], -'fgetss' => ['string|false', 'fp'=>'resource', 'length='=>'int', 'allowable_tags='=>'string'], +'fgetcsv' => ['(?array)|(?false)', 'fp'=>'resource', 'length='=>'0|positive-int', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], +'fgets' => ['string|false', 'fp'=>'resource', 'length='=>'0|positive-int'], +'fgetss' => ['string|false', 'fp'=>'resource', 'length='=>'0|positive-int', 'allowable_tags='=>'string'], 'file' => ['array|false', 'filename'=>'string', 'flags='=>'int', 'context='=>'resource'], 'file_exists' => ['bool', 'filename'=>'string'], -'file_get_contents' => ['string|false', 'filename'=>'string', 'use_include_path='=>'bool', 'context='=>'?resource', 'offset='=>'int', 'maxlen='=>'int'], -'file_put_contents' => ['int|false', 'file'=>'string', 'data'=>'mixed', 'flags='=>'int', 'context='=>'?resource'], +'file_get_contents' => ['string|false', 'filename'=>'string', 'use_include_path='=>'bool', 'context='=>'?resource', 'offset='=>'int', 'maxlen='=>'0|positive-int'], +'file_put_contents' => ['0|positive-int|false', 'file'=>'string', 'data'=>'mixed', 'flags='=>'int', 'context='=>'?resource'], 'fileatime' => ['int|false', 'filename'=>'string'], 'filectime' => ['int|false', 'filename'=>'string'], 'filegroup' => ['int|false', 'filename'=>'string'], @@ -2958,7 +2954,7 @@ 'filepro_fieldwidth' => ['int', 'field_number'=>'int'], 'filepro_retrieve' => ['string', 'row_number'=>'int', 'field_number'=>'int'], 'filepro_rowcount' => ['int'], -'filesize' => ['int|false', 'filename'=>'string'], +'filesize' => ['0|positive-int|false', 'filename'=>'string'], 'FilesystemIterator::__construct' => ['void', 'path'=>'string', 'flags='=>'int'], 'FilesystemIterator::current' => ['string|SplFileInfo'], 'FilesystemIterator::getFlags' => ['int'], @@ -2974,7 +2970,7 @@ 'filter_list' => ['array'], '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'], +'FilterIterator::__construct' => ['void', 'iterator'=>'Iterator'], 'FilterIterator::accept' => ['bool'], 'FilterIterator::current' => ['mixed'], 'FilterIterator::getInnerIterator' => ['Iterator'], @@ -2994,23 +2990,23 @@ 'finfo_set_flags' => ['bool', 'finfo'=>'resource', 'options'=>'int'], 'floatval' => ['float', 'var'=>'mixed'], 'flock' => ['bool', 'fp'=>'resource', 'operation'=>'int', '&w_wouldblock='=>'int'], -'floor' => ['float', 'number'=>'float'], +'floor' => ['float|false', 'number'=>'float'], 'flush' => ['void'], 'fmod' => ['float', 'x'=>'float', 'y'=>'float'], 'fnmatch' => ['bool', 'pattern'=>'string', 'filename'=>'string', 'flags='=>'int'], 'fopen' => ['resource|false', 'filename'=>'string', 'mode'=>'string', 'use_include_path='=>'bool', 'context='=>'resource'], 'forward_static_call' => ['mixed', 'function'=>'callable', '...parameters='=>'mixed'], 'forward_static_call_array' => ['mixed', 'function'=>'callable', 'parameters'=>'array'], -'fpassthru' => ['int|false', 'fp'=>'resource'], +'fpassthru' => ['0|positive-int|false', 'fp'=>'resource'], 'fpm_get_status' => ['array|false'], 'fprintf' => ['int', 'stream'=>'resource', 'format'=>'string', '...values='=>'string|int|float'], -'fputcsv' => ['int|false', 'fp'=>'resource', 'fields'=>'array', 'delimiter='=>'string', 'enclosure='=>'string', 'escape_char='=>'string'], -'fputs' => ['int|false', 'fp'=>'resource', 'str'=>'string', 'length='=>'int'], -'fread' => ['string|false', 'fp'=>'resource', 'length'=>'int'], +'fputcsv' => ['0|positive-int|false', 'fp'=>'resource', 'fields'=>'array', 'delimiter='=>'string', 'enclosure='=>'string', 'escape_char='=>'string'], +'fputs' => ['0|positive-int|false', 'fp'=>'resource', 'str'=>'string', 'length='=>'0|positive-int'], +'fread' => ['string|false', 'fp'=>'resource', 'length'=>'0|positive-int'], 'frenchtojd' => ['int', 'month'=>'int', 'day'=>'int', 'year'=>'int'], 'fribidi_log2vis' => ['string', 'str'=>'string', 'direction'=>'string', 'charset'=>'int'], 'fscanf' => ['array|int|false', 'stream'=>'resource', 'format'=>'string', '&...w_vars='=>'string|int|float|null'], -'fseek' => ['int', 'fp'=>'resource', 'offset'=>'int', 'whence='=>'int'], +'fseek' => ['0|-1', 'fp'=>'resource', 'offset'=>'int', 'whence='=>'int'], 'fsockopen' => ['resource|false', 'hostname'=>'string', 'port='=>'int', '&w_errno='=>'int', '&w_errstr='=>'string', 'timeout='=>'float'], 'fstat' => ['array|false', 'fp'=>'resource'], 'ftell' => ['int|false', 'fp'=>'resource'], @@ -3050,12 +3046,12 @@ 'ftp_size' => ['int', 'stream'=>'resource', 'filename'=>'string'], 'ftp_ssl_connect' => ['resource|false', 'host'=>'string', 'port='=>'int', 'timeout='=>'int'], 'ftp_systype' => ['string|false', 'stream'=>'resource'], -'ftruncate' => ['bool', 'fp'=>'resource', 'size'=>'int'], -'func_get_arg' => ['mixed', 'arg_num'=>'int'], +'ftruncate' => ['bool', 'fp'=>'resource', 'size'=>'0|positive-int'], +'func_get_arg' => ['mixed', 'arg_num'=>'0|positive-int'], 'func_get_args' => ['array'], -'func_num_args' => ['int'], +'func_num_args' => ['0|positive-int'], 'function_exists' => ['bool', 'function_name'=>'string'], -'fwrite' => ['int|false', 'fp'=>'resource', 'str'=>'string', 'length='=>'int'], +'fwrite' => ['0|positive-int|false', 'fp'=>'resource', 'str'=>'string', 'length='=>'0|positive-int'], 'gc_collect_cycles' => ['int'], 'gc_disable' => ['void'], 'gc_enable' => ['void'], @@ -3311,23 +3307,23 @@ 'get_defined_constants' => ['array', 'categorize='=>'bool'], 'get_defined_functions' => ['array>', 'exclude_disabled='=>'bool'], 'get_defined_vars' => ['array'], -'get_extension_funcs' => ['array|false', 'extension_name'=>'string'], +'get_extension_funcs' => ['list|false', 'extension_name'=>'string'], 'get_headers' => ['array|false', 'url'=>'string', 'format='=>'int', 'context='=>'resource'], 'get_html_translation_table' => ['array', 'table='=>'int', 'flags='=>'int', 'encoding='=>'string'], 'get_include_path' => ['string|false'], -'get_included_files' => ['array'], -'get_loaded_extensions' => ['array', 'zend_extensions='=>'bool'], -'get_magic_quotes_gpc' => ['bool'], -'get_magic_quotes_runtime' => ['bool'], +'get_included_files' => ['list'], +'get_loaded_extensions' => ['list', 'zend_extensions='=>'bool'], +'get_magic_quotes_gpc' => ['false'], +'get_magic_quotes_runtime' => ['false'], 'get_meta_tags' => ['array|false', 'filename'=>'string', 'use_include_path='=>'bool'], 'get_object_vars' => ['array', 'obj'=>'object'], 'get_parent_class' => ['class-string|false', 'object='=>'mixed'], -'get_required_files' => ['string[]'], +'get_required_files' => ['list'], 'get_resource_type' => ['string', 'res'=>'resource'], -'get_resources' => ['resource[]', 'resource_type'=>'string'], +'get_resources' => ['array', 'type='=>'string'], 'getallheaders' => ['array'], 'getcwd' => ['string|false'], -'getdate' => ['array', 'timestamp='=>'int'], +'getdate' => ['array{seconds: int<0, 59>, minutes: int<0, 59>, hours: int<0, 23>, mday: int<1, 31>, wday: int<0, 6>, mon: int<1, 12>, year: int, yday: int<0, 365>, weekday: "Monday"|"Tuesday"|"Wednesday"|"Thursday"|"Friday"|"Saturday"|"Sunday", month: "January"|"February"|"March"|"April"|"May"|"June"|"July"|"August"|"September"|"October"|"November"|"December", 0: int}', 'timestamp='=>'int'], 'getenv' => ['string|false', 'varname'=>'string', 'local_only='=>'bool'], 'getenv\'1' => ['string[]'], 'gethostbyaddr' => ['string|false', 'ip_address'=>'string'], @@ -3342,7 +3338,7 @@ 'getmyinode' => ['int|false'], 'getmypid' => ['int|false'], 'getmyuid' => ['int|false'], -'getopt' => ['array|array|array>|false', 'options'=>'string', 'longopts='=>'array', '&w_optind='=>'int'], +'getopt' => ['__benevolent|array|array>|false>', 'options'=>'string', 'longopts='=>'array', '&w_optind='=>'int'], 'getprotobyname' => ['int|false', 'name'=>'string'], 'getprotobynumber' => ['string|false', 'proto'=>'int'], 'getrandmax' => ['int'], @@ -3550,7 +3546,7 @@ 'gmp_clrbit' => ['void', 'a'=>'GMP|string|int', 'index'=>'int'], 'gmp_cmp' => ['int', 'a'=>'GMP|string|int', 'b'=>'GMP|string|int'], 'gmp_com' => ['GMP', 'a'=>'GMP|string|int'], -'gmp_div' => ['GMP', 'a'=>'GMP|resource|string', 'b'=>'GMP|resource|string', 'round='=>'int'], +'gmp_div' => ['GMP', 'a'=>'GMP|string|int', 'b'=>'GMP|string|int', 'round='=>'int'], 'gmp_div_q' => ['GMP', 'a'=>'GMP|string|int', 'b'=>'GMP|string|int', 'round='=>'int'], 'gmp_div_qr' => ['array', 'a'=>'GMP|string|int', 'b'=>'GMP|string|int', 'round='=>'int'], 'gmp_div_r' => ['GMP', 'a'=>'GMP|string|int', 'b'=>'GMP|string|int', 'round='=>'int'], @@ -3616,7 +3612,7 @@ 'gnupg::seterrormode' => ['void', 'errormode'=>'int'], 'gnupg::setsignmode' => ['bool', 'signmode'=>'int'], 'gnupg::sign' => ['string', 'plaintext'=>'string'], -'gnupg::verify' => ['array', 'signed_text'=>'string', 'signature'=>'string', '&plaintext='=>'string'], +'gnupg::verify' => ['array', 'signed_text'=>'string', 'signature'=>'string|false', '&plaintext='=>'string'], 'gnupg_adddecryptkey' => ['bool', 'identifier'=>'resource', 'fingerprint'=>'string', 'passphrase'=>'string'], 'gnupg_addencryptkey' => ['bool', 'identifier'=>'resource', 'fingerprint'=>'string'], 'gnupg_addsignkey' => ['bool', 'identifier'=>'resource', 'fingerprint'=>'string', 'passphrase='=>'string'], @@ -3637,12 +3633,12 @@ 'gnupg_seterrormode' => ['void', 'identifier'=>'resource', 'errormode'=>'int'], 'gnupg_setsignmode' => ['bool', 'identifier'=>'resource', 'signmode'=>'int'], 'gnupg_sign' => ['string', 'identifier'=>'resource', 'plaintext'=>'string'], -'gnupg_verify' => ['array', 'identifier'=>'resource', 'signed_text'=>'string', 'signature'=>'string', 'plaintext='=>'string'], +'gnupg_verify' => ['array', 'identifier'=>'resource', 'signed_text'=>'string', 'signature'=>'string|false', '&plaintext='=>'string'], 'gopher_parsedir' => ['array', 'dirent'=>'string'], 'grapheme_extract' => ['string|false', 'str'=>'string', 'size'=>'int', 'extract_type='=>'int', 'start='=>'int', '&w_next='=>'int'], 'grapheme_stripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], 'grapheme_stristr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'part='=>'bool'], -'grapheme_strlen' => ['int|false', 'str'=>'string'], +'grapheme_strlen' => ['0|positive-int|false', 'str'=>'string'], 'grapheme_strpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], 'grapheme_strripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], 'grapheme_strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], @@ -3665,7 +3661,7 @@ 'Grpc\ChannelCredentials::createComposite' => ['Grpc\ChannelCredentials', 'cred1'=>'Grpc\ChannelCredentials', 'cred2'=>'Grpc\CallCredentials'], 'Grpc\ChannelCredentials::createDefault' => ['Grpc\ChannelCredentials'], 'Grpc\ChannelCredentials::createInsecure' => ['null'], -'Grpc\ChannelCredentials::createSsl' => ['Grpc\ChannelCredentials', 'pem_root_certs'=>'string', 'pem_private_key='=>'string', 'pem_cert_chain='=>'string'], +'Grpc\ChannelCredentials::createSsl' => ['Grpc\ChannelCredentials', 'pem_root_certs='=>'string|null', 'pem_private_key='=>'string|null', 'pem_cert_chain='=>'string|null'], 'Grpc\ChannelCredentials::setDefaultRootsPem' => ['', 'pem_roots'=>'string'], 'Grpc\Server::__construct' => ['void', 'args'=>'array'], 'Grpc\Server::addHttp2Port' => ['bool', 'addr'=>'string'], @@ -3727,8 +3723,8 @@ 'gzdecode' => ['string|false', 'data'=>'string', 'length='=>'int'], 'gzdeflate' => ['string|false', 'data'=>'string', 'level='=>'int', 'encoding='=>'int'], 'gzencode' => ['string|false', 'data'=>'string', 'level='=>'int', 'encoding_mode='=>'int'], -'gzeof' => ['int', 'zp'=>'resource'], -'gzfile' => ['array|false', 'filename'=>'string', 'use_include_path='=>'int'], +'gzeof' => ['bool', 'zp'=>'resource'], +'gzfile' => ['list|false', 'filename'=>'string', 'use_include_path='=>'int'], 'gzgetc' => ['string|false', 'zp'=>'resource'], 'gzgets' => ['string|false', 'zp'=>'resource', 'length='=>'int'], 'gzgetss' => ['string|false', 'zp'=>'resource', 'length'=>'int', 'allowable_tags='=>'string'], @@ -3906,18 +3902,18 @@ 'HaruPage::stroke' => ['bool', 'close_path='=>'bool'], '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' => ['string|false', 'algo'=>'string', 'data'=>'string', 'raw_output='=>'bool'], -'hash_algos' => ['array'], +'hash' => ['non-empty-string|false', 'algo'=>'string', 'data'=>'string', 'raw_output='=>'bool'], +'hash_algos' => ['array'], 'hash_copy' => ['HashContext', 'context'=>'HashContext'], 'hash_equals' => ['bool', 'known_string'=>'string', 'user_string'=>'string'], -'hash_file' => ['string|false', 'algo'=>'string', 'filename'=>'string', 'raw_output='=>'bool'], -'hash_final' => ['string', 'context'=>'HashContext', 'raw_output='=>'bool'], -'hash_hkdf' => ['string', 'algo'=>'string', 'ikm'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], -'hash_hmac' => ['string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'raw_output='=>'bool'], -'hash_hmac_algos' => ['array'], -'hash_hmac_file' => ['string|false', 'algo'=>'string', 'filename'=>'string', 'key'=>'string', 'raw_output='=>'bool'], +'hash_file' => ['non-empty-string|false', 'algo'=>'string', 'filename'=>'string', 'raw_output='=>'bool'], +'hash_final' => ['non-empty-string', 'context'=>'HashContext', 'raw_output='=>'bool'], +'hash_hkdf' => ['non-empty-string|false', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], +'hash_hmac' => ['non-empty-string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'raw_output='=>'bool'], +'hash_hmac_algos' => ['array'], +'hash_hmac_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' => ['string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'raw_output='=>'bool'], +'hash_pbkdf2' => ['non-empty-string|false', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'raw_output='=>'bool'], 'hash_update' => ['bool', 'context'=>'HashContext', 'data'=>'string'], 'hash_update_file' => ['bool', 'context'=>'HashContext', 'filename'=>'string', 'scontext='=>'?HashContext'], 'hash_update_stream' => ['int', 'context'=>'HashContext', 'handle'=>'resource', 'length='=>'int'], @@ -4452,7 +4448,7 @@ 'iconv_mime_decode_headers' => ['array|false', 'headers'=>'string', 'mode='=>'int', 'charset='=>'string'], 'iconv_mime_encode' => ['string|false', 'field_name'=>'string', 'field_value'=>'string', 'preference='=>'array'], 'iconv_set_encoding' => ['bool', 'type'=>'string', 'charset'=>'string'], -'iconv_strlen' => ['int|false', 'str'=>'string', 'charset='=>'string'], +'iconv_strlen' => ['0|positive-int|false', 'str'=>'string', 'charset='=>'string'], 'iconv_strpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'charset='=>'string'], 'iconv_strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'charset='=>'string'], 'iconv_substr' => ['string|false', 'str'=>'string', 'offset'=>'int', 'length='=>'int', 'charset='=>'string'], @@ -4507,8 +4503,8 @@ 'ifxus_seek_slob' => ['int', 'bid'=>'int', 'mode'=>'int', 'offset'=>'int'], 'ifxus_tell_slob' => ['int', 'bid'=>'int'], 'ifxus_write_slob' => ['int', 'bid'=>'int', 'content'=>'string'], -'igbinary_serialize' => ['string', 'value'=>''], -'igbinary_unserialize' => ['', 'str'=>'string'], +'igbinary_serialize' => ['string|null', 'value'=>'mixed'], +'igbinary_unserialize' => ['mixed', 'str'=>'string'], 'ignore_user_abort' => ['int', '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'], @@ -4602,7 +4598,6 @@ 'imageinterlace' => ['int', 'im'=>'resource', 'interlace='=>'int'], 'imageistruecolor' => ['bool', 'im'=>'resource'], 'imagejpeg' => ['bool', 'im'=>'resource', 'filename='=>'string|resource|null', 'quality='=>'int'], -'imagejpeg\'1' => ['string|false', 'im'=>'resource', 'filename='=>'null', 'quality='=>'int'], 'imagelayereffect' => ['bool', 'im'=>'resource', 'effect'=>'int'], 'imageline' => ['bool', 'im'=>'resource', 'x1'=>'int', 'y1'=>'int', 'x2'=>'int', 'y2'=>'int', 'col'=>'int'], 'imageloadfont' => ['int|false', 'filename'=>'string'], @@ -5214,7 +5209,7 @@ 'imap_close' => ['bool', 'stream_id'=>'resource', 'options='=>'int'], 'imap_create' => ['bool', 'stream_id'=>'resource', 'mailbox'=>'string'], 'imap_createmailbox' => ['bool', 'stream_id'=>'resource', 'mailbox'=>'string'], -'imap_delete' => ['bool', 'stream_id'=>'resource', 'msg_no'=>'int', 'options='=>'int'], +'imap_delete' => ['bool', 'stream_id'=>'resource', 'msg_no'=>'string', 'options='=>'int'], 'imap_deletemailbox' => ['bool', 'stream_id'=>'resource', 'mailbox'=>'string'], 'imap_errors' => ['array|false'], 'imap_expunge' => ['bool', 'stream_id'=>'resource'], @@ -5271,7 +5266,7 @@ 'imap_thread' => ['array|false', 'stream_id'=>'resource', 'options='=>'int'], 'imap_timeout' => ['mixed', 'timeout_type'=>'int', 'timeout='=>'int'], 'imap_uid' => ['int|false', 'stream_id'=>'resource', 'msg_no'=>'int'], -'imap_undelete' => ['bool', 'stream_id'=>'resource', 'msg_no'=>'int', 'flags='=>'int'], +'imap_undelete' => ['bool', 'stream_id'=>'resource', 'msg_no'=>'string', 'flags='=>'int'], 'imap_unsubscribe' => ['bool', 'stream_id'=>'resource', 'mailbox'=>'string'], 'imap_utf7_decode' => ['string|false', 'buf'=>'string'], 'imap_utf7_encode' => ['string', 'buf'=>'string'], @@ -5285,7 +5280,7 @@ 'inclued_get_data' => ['array'], 'inet_ntop' => ['string|false', 'in_addr'=>'string'], 'inet_pton' => ['string|false', 'ip_address'=>'string'], -'InfiniteIterator::__construct' => ['void', 'iterator'=>'iterator'], +'InfiniteIterator::__construct' => ['void', 'iterator'=>'Iterator'], 'InfiniteIterator::next' => ['void'], 'inflate_add' => ['string|false', 'context'=>'resource', 'encoded_data'=>'string', 'flush_mode='=>'int'], 'inflate_get_read_len' => ['int|false', 'resource'=>'resource'], @@ -5621,7 +5616,7 @@ 'InvalidArgumentException::getLine' => ['int'], 'InvalidArgumentException::getMessage' => ['string'], 'InvalidArgumentException::getPrevious' => ['Throwable|InvalidArgumentException|null'], -'InvalidArgumentException::getTrace' => ['array'], +'InvalidArgumentException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'InvalidArgumentException::getTraceAsString' => ['string'], 'ip2long' => ['int|false', 'ip_address'=>'string'], 'iptcembed' => ['string|bool', 'iptcdata'=>'string', 'jpeg_file_name'=>'string', 'spool='=>'int'], @@ -5658,14 +5653,13 @@ 'is_uploaded_file' => ['bool', 'path'=>'string'], 'is_writable' => ['bool', 'filename'=>'string'], 'is_writeable' => ['bool', 'filename'=>'string'], -'isset' => ['bool', 'var'=>'mixed', '...rest='=>'mixed'], 'Iterator::current' => ['mixed'], 'Iterator::key' => ['mixed'], 'Iterator::next' => ['void'], 'Iterator::rewind' => ['void'], 'Iterator::valid' => ['bool'], -'iterator_apply' => ['int', 'iterator'=>'Traversable', 'function'=>'callable', 'params='=>'array'], -'iterator_count' => ['int', 'iterator'=>'Traversable'], +'iterator_apply' => ['0|positive-int', 'iterator'=>'Traversable', 'function'=>'callable', 'params='=>'array'], +'iterator_count' => ['0|positive-int', 'iterator'=>'Traversable'], 'iterator_to_array' => ['array', 'iterator'=>'Traversable', 'use_keys='=>'bool'], 'IteratorAggregate::getIterator' => ['Traversable'], 'IteratorIterator::__construct' => ['void', 'iterator'=>'Traversable'], @@ -5695,8 +5689,8 @@ 'join' => ['string', 'glue'=>'string', 'pieces'=>'array'], 'join\'1' => ['string', 'pieces'=>'array'], 'jpeg2wbmp' => ['bool', 'jpegname'=>'string', 'wbmpname'=>'string', 'dest_height'=>'int', 'dest_width'=>'int', 'threshold'=>'int'], -'json_decode' => ['mixed', 'json'=>'string', 'assoc='=>'bool', 'depth='=>'int', 'options='=>'int'], -'json_encode' => ['string|false', 'data'=>'mixed', 'options='=>'int', 'depth='=>'int'], +'json_decode' => ['mixed', 'json'=>'string', 'assoc='=>'bool', 'depth='=>'positive-int', 'options='=>'int'], +'json_encode' => ['string|false', 'data'=>'mixed', 'options='=>'int', 'depth='=>'positive-int'], 'json_last_error' => ['int'], 'json_last_error_msg' => ['string'], 'JsonIncrementalParser::__construct' => ['void', 'depth'=>'', 'options'=>''], @@ -5739,7 +5733,7 @@ 'kadm5_get_principals' => ['array', 'handle'=>'resource'], 'kadm5_init_with_password' => ['resource', 'admin_server'=>'string', 'realm'=>'string', 'principal'=>'string', 'password'=>'string'], 'kadm5_modify_principal' => ['bool', 'handle'=>'resource', 'principal'=>'string', 'options'=>'array'], -'key' => ['int|string|null', 'array_arg'=>'array'], +'key' => ['int|string|null', 'array_arg'=>'array|object'], 'key_exists' => ['bool', 'key'=>'string|int', 'search'=>'array'], 'krsort' => ['bool', '&rw_array_arg'=>'array', 'sort_flags='=>'int'], 'ksort' => ['bool', '&rw_array_arg'=>'array', 'sort_flags='=>'int'], @@ -5900,7 +5894,7 @@ 'ldap_sasl_bind' => ['bool', 'link_identifier'=>'resource', 'binddn='=>'string', 'password='=>'string', 'sasl_mech='=>'string', 'sasl_realm='=>'string', 'sasl_authc_id='=>'string', 'sasl_authz_id='=>'string', 'props='=>'string'], 'ldap_search' => ['resource|false', 'link_identifier'=>'resource|array', 'base_dn'=>'string', 'filter'=>'string', 'attrs='=>'array', 'attrsonly='=>'int', 'sizelimit='=>'int', 'timelimit='=>'int', 'deref='=>'int', 'servercontrols='=>'array'], 'ldap_set_option' => ['bool', 'link_identifier'=>'resource|null', 'option'=>'int', 'newval'=>'mixed'], -'ldap_set_rebind_proc' => ['bool', 'link_identifier'=>'resource', 'callback'=>'string'], +'ldap_set_rebind_proc' => ['bool', 'link_identifier'=>'resource', 'callback'=>'callable'], 'ldap_sort' => ['bool', 'link_identifier'=>'resource', 'result_identifier'=>'resource', 'sortfilter'=>'string'], 'ldap_start_tls' => ['bool', 'link_identifier'=>'resource'], 'ldap_t61_to_8859' => ['string|false', 'value'=>'string'], @@ -5919,7 +5913,7 @@ 'LengthException::getLine' => ['int'], 'LengthException::getMessage' => ['string'], 'LengthException::getPrevious' => ['Throwable|LengthException|null'], -'LengthException::getTrace' => ['array'], +'LengthException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'LengthException::getTraceAsString' => ['string'], 'levenshtein' => ['int', 'str1'=>'string', 'str2'=>'string'], 'levenshtein\'1' => ['int', 'str1'=>'string', 'str2'=>'string', 'cost_ins'=>'int', 'cost_rep'=>'int', 'cost_del'=>'int'], @@ -5999,7 +5993,7 @@ 'LogicException::getLine' => ['int'], 'LogicException::getMessage' => ['string'], 'LogicException::getPrevious' => ['Throwable|LogicException|null'], -'LogicException::getTrace' => ['array'], +'LogicException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'LogicException::getTraceAsString' => ['string'], 'long2ip' => ['string|false', 'proper_address'=>'int'], 'lstat' => ['array|false', 'filename'=>'string'], @@ -6323,8 +6317,8 @@ 'mb_encoding_aliases' => ['array|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', 'pattern'=>'string', 'replacement'=>'string', 'string'=>'string', 'option='=>'string'], -'mb_ereg_replace_callback' => ['string|false', 'pattern'=>'string', 'callback'=>'callable', 'string'=>'string', 'option='=>'string'], +'mb_ereg_replace' => ['string|false|null', 'pattern'=>'string', 'replacement'=>'string', 'string'=>'string', 'option='=>'string'], +'mb_ereg_replace_callback' => ['string|false|null', 'pattern'=>'string', 'callback'=>'callable', 'string'=>'string', 'option='=>'string'], 'mb_ereg_search' => ['bool', 'pattern='=>'string', 'option='=>'string'], 'mb_ereg_search_getpos' => ['int'], 'mb_ereg_search_getregs' => ['array|false'], @@ -6348,24 +6342,24 @@ 'mb_regex_set_options' => ['string', 'options='=>'string'], 'mb_scrub' => ['string', 'str'=>'string', 'enc='=>'string'], 'mb_send_mail' => ['bool', 'to'=>'string', 'subject'=>'string', 'message'=>'string', 'additional_headers='=>'string|array|null', 'additional_parameter='=>'string'], -'mb_split' => ['array|false', 'pattern'=>'string', 'string'=>'string', 'limit='=>'int'], +'mb_split' => ['list|false', 'pattern'=>'string', 'string'=>'string', 'limit='=>'int'], 'mb_strcut' => ['string', 'str'=>'string', 'start'=>'int', 'length='=>'?int', 'encoding='=>'string'], 'mb_strimwidth' => ['string', 'str'=>'string', 'start'=>'int', 'width'=>'int', 'trimmarker='=>'string', 'encoding='=>'string'], -'mb_stripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], +'mb_stripos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], 'mb_stristr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'part='=>'bool', 'encoding='=>'string'], -'mb_strlen' => ['int|false', 'str'=>'string', 'encoding='=>'string'], -'mb_strpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], +'mb_strlen' => ['0|positive-int|false', 'str'=>'string', 'encoding='=>'string'], +'mb_strpos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], 'mb_strrchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'part='=>'bool', 'encoding='=>'string'], 'mb_strrichr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'part='=>'bool', 'encoding='=>'string'], -'mb_strripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], -'mb_strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], +'mb_strripos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], +'mb_strrpos' => ['0|positive-int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'], 'mb_strstr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'part='=>'bool', 'encoding='=>'string'], 'mb_strtolower' => ['string', 'str'=>'string', 'encoding='=>'string'], 'mb_strtoupper' => ['string', 'str'=>'string', 'encoding='=>'string'], -'mb_strwidth' => ['int', 'str'=>'string', 'encoding='=>'string'], +'mb_strwidth' => ['0|positive-int', 'str'=>'string', 'encoding='=>'string'], 'mb_substitute_character' => ['mixed', 'substchar='=>'mixed'], 'mb_substr' => ['string', 'str'=>'string', 'start'=>'int', 'length='=>'?int', 'encoding='=>'string'], -'mb_substr_count' => ['int', 'haystack'=>'string', 'needle'=>'string', 'encoding='=>'string'], +'mb_substr_count' => ['0|positive-int', 'haystack'=>'string', 'needle'=>'string', 'encoding='=>'string'], 'mcrypt_cbc' => ['string', 'cipher'=>'string', 'key'=>'string', 'data'=>'string', 'mode'=>'int', 'iv='=>'string'], 'mcrypt_cfb' => ['string', 'cipher'=>'string', 'key'=>'string', 'data'=>'string', 'mode'=>'int', 'iv='=>'string'], 'mcrypt_create_iv' => ['string', 'size'=>'int', 'source='=>'int'], @@ -6402,8 +6396,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' => ['string', 'str'=>'string', 'raw_output='=>'bool'], -'md5_file' => ['string|false', 'filename'=>'string', 'raw_output='=>'bool'], +'md5' => ['non-empty-string', 'str'=>'string', 'raw_output='=>'bool'], +'md5_file' => ['non-empty-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'], @@ -6412,7 +6406,7 @@ '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' => ['string|array|false', 'key'=>'string', '&flags='=>'array', '&keys='=>'array'], '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'], @@ -6436,7 +6430,7 @@ 'Memcached::decrementByKey' => ['int|false', 'server_key'=>'string', 'key'=>'string', 'offset='=>'int', 'initial_value='=>'int', 'expiry='=>'int'], 'Memcached::delete' => ['bool', 'key'=>'string', 'time='=>'int'], 'Memcached::deleteByKey' => ['bool', 'server_key'=>'string', 'key'=>'string', 'time='=>'int'], -'Memcached::deleteMulti' => ['bool', 'keys'=>'array', 'time='=>'int'], +'Memcached::deleteMulti' => ['array', 'keys'=>'array', 'time='=>'int'], 'Memcached::deleteMultiByKey' => ['bool', 'server_key'=>'string', 'keys'=>'array', 'time='=>'int'], 'Memcached::fetch' => ['array'], 'Memcached::fetchAll' => ['array'], @@ -6479,7 +6473,7 @@ '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' => ['string|array|false', 'key'=>'string', '&flags='=>'array', '&keys='=>'array'], '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'], @@ -6663,7 +6657,7 @@ 'MongoCursorException::getLine' => ['int'], 'MongoCursorException::getMessage' => ['string'], 'MongoCursorException::getPrevious' => ['Exception|Throwable'], -'MongoCursorException::getTrace' => ['array'], +'MongoCursorException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'MongoCursorException::getTraceAsString' => ['string'], 'MongoCursorInterface::__construct' => ['void'], 'MongoCursorInterface::batchSize' => ['MongoCursorInterface', 'batchSize'=>'int'], @@ -6755,7 +6749,7 @@ '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'], +'MongoDB\Driver\Exception\RuntimeException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,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)'], @@ -6766,7 +6760,7 @@ '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'], +'MongoDB\Driver\Exception\WriteException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'MongoDB\Driver\Exception\WriteException::getTraceAsString' => ['string'], 'MongoDB\Driver\Exception\WriteException::getWriteResult' => ['MongoDB\Driver\WriteResult'], 'MongoDB\Driver\Manager::__construct' => ['void', 'uri'=>'string', 'options='=>'array', 'driverOptions='=>'array'], @@ -6842,7 +6836,7 @@ 'MongoException::getLine' => ['int'], 'MongoException::getMessage' => ['string'], 'MongoException::getPrevious' => ['Exception|Throwable'], -'MongoException::getTrace' => ['array'], +'MongoException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'MongoException::getTraceAsString' => ['string'], 'MongoGridFS::__construct' => ['void', 'db'=>'MongoDB', 'prefix='=>'string', 'chunks='=>'mixed'], 'MongoGridFS::__get' => ['MongoCollection', 'name'=>'string'], @@ -6953,7 +6947,7 @@ 'MongoResultException::getLine' => ['int'], 'MongoResultException::getMessage' => ['string'], 'MongoResultException::getPrevious' => ['Exception|Throwable'], -'MongoResultException::getTrace' => ['array'], +'MongoResultException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'MongoResultException::getTraceAsString' => ['string'], 'MongoTimestamp::__construct' => ['void', 'sec='=>'int', 'inc='=>'int'], 'MongoTimestamp::__toString' => ['string'], @@ -6973,7 +6967,7 @@ 'MongoWriteConcernException::getLine' => ['int'], 'MongoWriteConcernException::getMessage' => ['string'], 'MongoWriteConcernException::getPrevious' => ['Exception|Throwable'], -'MongoWriteConcernException::getTrace' => ['array'], +'MongoWriteConcernException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'MongoWriteConcernException::getTraceAsString' => ['string'], 'monitor_custom_event' => ['void', 'class'=>'string', 'text'=>'string', 'severe='=>'int', 'user_data='=>'mixed'], 'monitor_httperror_event' => ['void', 'error_code'=>'int', 'url'=>'string', 'severe='=>'int'], @@ -7108,11 +7102,11 @@ 'mt_rand\'1' => ['int'], 'mt_srand' => ['void', 'seed='=>'int', 'mode='=>'int'], 'MultipleIterator::__construct' => ['void', 'flags='=>'int'], -'MultipleIterator::attachIterator' => ['void', 'iterator'=>'iterator', 'infos='=>'string'], -'MultipleIterator::containsIterator' => ['bool', 'iterator'=>'iterator'], +'MultipleIterator::attachIterator' => ['void', 'iterator'=>'Iterator', 'infos='=>'string'], +'MultipleIterator::containsIterator' => ['bool', 'iterator'=>'Iterator'], 'MultipleIterator::countIterators' => ['int'], 'MultipleIterator::current' => ['array'], -'MultipleIterator::detachIterator' => ['void', 'iterator'=>'iterator'], +'MultipleIterator::detachIterator' => ['void', 'iterator'=>'Iterator'], 'MultipleIterator::getFlags' => ['int'], 'MultipleIterator::key' => ['array'], 'MultipleIterator::next' => ['void'], @@ -7240,7 +7234,7 @@ '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|false', 'result'=>'mysqli_result'], +'mysqli_fetch_fields' => ['array', '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_row' => ['array|null', 'result'=>'mysqli_result'], @@ -7291,7 +7285,7 @@ '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|false'], +'mysqli_result::fetch_fields' => ['array'], 'mysqli_result::fetch_object' => ['object|null', 'class_name='=>'string', 'params='=>'array'], 'mysqli_result::fetch_row' => ['array|null'], 'mysqli_result::field_seek' => ['bool', 'fieldnr'=>'int'], @@ -7341,10 +7335,10 @@ 'mysqli_stmt_data_seek' => ['void', 'stmt'=>'mysqli_stmt', 'offset'=>'int'], 'mysqli_stmt_errno' => ['int', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_error' => ['string', 'stmt'=>'mysqli_stmt'], -'mysqli_stmt_error_list' => ['array', 'stmt'=>'mysqli_stmt'], +'mysqli_stmt_error_list' => ['list', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_execute' => ['bool', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_fetch' => ['bool|null', 'stmt'=>'mysqli_stmt'], -'mysqli_stmt_field_count' => ['int', 'stmt'=>'mysqli_stmt'], +'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'], @@ -7352,13 +7346,13 @@ 'mysqli_stmt_insert_id' => ['', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_more_results' => ['bool', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_next_result' => ['bool', 'stmt'=>'mysqli_stmt'], -'mysqli_stmt_num_rows' => ['int', 'stmt'=>'mysqli_stmt'], -'mysqli_stmt_param_count' => ['int', 'stmt'=>'mysqli_stmt'], +'mysqli_stmt_num_rows' => ['0|positive-int', 'stmt'=>'mysqli_stmt'], +'mysqli_stmt_param_count' => ['0|positive-int', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_prepare' => ['bool', 'stmt'=>'mysqli_stmt', 'query'=>'string'], 'mysqli_stmt_reset' => ['bool', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_result_metadata' => ['mysqli_result|false', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_send_long_data' => ['bool', 'stmt'=>'mysqli_stmt', 'param_nr'=>'int', 'data'=>'string'], -'mysqli_stmt_sqlstate' => ['string', 'stmt'=>'mysqli_stmt'], +'mysqli_stmt_sqlstate' => ['non-empty-string', 'stmt'=>'mysqli_stmt'], 'mysqli_stmt_store_result' => ['bool', 'stmt'=>'mysqli_stmt'], 'mysqli_store_result' => ['mysqli_result|false', 'link'=>'mysqli', 'option='=>'int'], 'mysqli_thread_id' => ['int', 'link'=>'mysqli'], @@ -7751,13 +7745,13 @@ 'newt_win_message' => ['void', 'title'=>'string', 'button_text'=>'string', 'format'=>'string', 'args='=>'mixed', '...args='=>'mixed'], 'newt_win_messagev' => ['void', 'title'=>'string', 'button_text'=>'string', 'format'=>'string', 'args'=>'array'], 'newt_win_ternary' => ['int', 'title'=>'string', 'button1_text'=>'string', 'button2_text'=>'string', 'button3_text'=>'string', 'format'=>'string', 'args='=>'mixed', '...args='=>'mixed'], -'next' => ['mixed', '&rw_array_arg'=>'array'], +'next' => ['mixed', '&rw_array_arg'=>'array|object'], 'ngettext' => ['string', 'msgid1'=>'string', 'msgid2'=>'string', 'n'=>'int'], 'nl2br' => ['string', 'str'=>'string', 'is_xhtml='=>'bool'], 'nl_langinfo' => ['string|false', 'item'=>'int'], -'NoRewindIterator::__construct' => ['void', 'iterator'=>'iterator'], +'NoRewindIterator::__construct' => ['void', 'iterator'=>'Iterator'], 'NoRewindIterator::current' => ['mixed'], -'NoRewindIterator::getInnerIterator' => ['iterator'], +'NoRewindIterator::getInnerIterator' => ['Iterator'], 'NoRewindIterator::key' => ['mixed'], 'NoRewindIterator::next' => ['void'], 'NoRewindIterator::rewind' => ['void'], @@ -8117,7 +8111,7 @@ 'openssl_public_decrypt' => ['bool', 'data'=>'string', '&w_decrypted'=>'string', 'key'=>'string|resource', 'padding='=>'int'], 'openssl_public_encrypt' => ['bool', 'data'=>'string', '&w_crypted'=>'string', 'key'=>'string|resource', 'padding='=>'int'], 'openssl_random_pseudo_bytes' => ['string|false', 'length'=>'int', '&w_crypto_strong='=>'bool'], -'openssl_seal' => ['int|false', 'data'=>'string', '&w_sealed_data'=>'string', '&w_env_keys'=>'array', 'pub_key_ids'=>'array', 'method='=>'string', '&rw_iv='=>'string'], +'openssl_seal' => ['int|false', 'data'=>'string', '&w_sealed_data'=>'string', '&w_env_keys'=>'array', 'pub_key_ids'=>'array', 'method='=>'string', '&w_iv='=>'string'], 'openssl_sign' => ['bool', 'data'=>'string', '&w_signature'=>'string', 'priv_key_id'=>'resource|string', 'signature_alg='=>'int|string'], 'openssl_spki_export' => ['string|null|false', 'spkac'=>'string'], 'openssl_spki_export_challenge' => ['string|null|false', 'spkac'=>'string'], @@ -8142,7 +8136,7 @@ 'OutOfBoundsException::getLine' => ['int'], 'OutOfBoundsException::getMessage' => ['string'], 'OutOfBoundsException::getPrevious' => ['Throwable|OutOfBoundsException|null'], -'OutOfBoundsException::getTrace' => ['array'], +'OutOfBoundsException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'OutOfBoundsException::getTraceAsString' => ['string'], 'OutOfRangeException::__clone' => ['void'], 'OutOfRangeException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Throwable)|(?OutOfRangeException)'], @@ -8152,7 +8146,7 @@ 'OutOfRangeException::getLine' => ['int'], 'OutOfRangeException::getMessage' => ['string'], 'OutOfRangeException::getPrevious' => ['Throwable|OutOfRangeException|null'], -'OutOfRangeException::getTrace' => ['array'], +'OutOfRangeException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'OutOfRangeException::getTraceAsString' => ['string'], 'output_add_rewrite_var' => ['bool', 'name'=>'string', 'value'=>'string'], 'output_reset_rewrite_vars' => ['bool'], @@ -8164,12 +8158,12 @@ 'OverflowException::getLine' => ['int'], 'OverflowException::getMessage' => ['string'], 'OverflowException::getPrevious' => ['Throwable|OverflowException|null'], -'OverflowException::getTrace' => ['array'], +'OverflowException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'OverflowException::getTraceAsString' => ['string'], 'overload' => ['', 'class_name'=>'string'], 'override_function' => ['bool', 'function_name'=>'string', 'function_args'=>'string', 'function_code'=>'string'], 'pack' => ['string', 'format'=>'string', '...args='=>'mixed'], -'ParentIterator::__construct' => ['void', 'iterator'=>'recursiveiterator'], +'ParentIterator::__construct' => ['void', 'iterator'=>'RecursiveIterator'], 'ParentIterator::accept' => ['bool'], 'ParentIterator::getChildren' => ['ParentIterator'], 'ParentIterator::hasChildren' => ['bool'], @@ -8231,7 +8225,7 @@ 'parse_ini_file' => ['array|false', 'filename'=>'string', 'process_sections='=>'bool', 'scanner_mode='=>'int'], 'parse_ini_string' => ['array|false', 'ini_string'=>'string', 'process_sections='=>'bool', 'scanner_mode='=>'int'], 'parse_str' => ['void', 'encoded_string'=>'string', '&w_result='=>'array'], -'parse_url' => ['mixed', 'url'=>'string', 'url_component='=>'int'], +'parse_url' => ['array|int|string|false|null', 'url'=>'string', 'url_component='=>'int'], 'ParseError::__clone' => ['void'], 'ParseError::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Throwable)|(?ParseError)'], 'ParseError::__toString' => ['string'], @@ -8240,7 +8234,7 @@ 'ParseError::getLine' => ['int'], 'ParseError::getMessage' => ['string'], 'ParseError::getPrevious' => ['Throwable|ParseError|null'], -'ParseError::getTrace' => ['array'], +'ParseError::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'ParseError::getTraceAsString' => ['string'], 'parsekit_compile_file' => ['array', 'filename'=>'string', 'errors='=>'array', 'options='=>'int'], 'parsekit_compile_string' => ['array', 'phpcode'=>'string', 'errors='=>'array', 'options='=>'int'], @@ -8444,17 +8438,17 @@ 'PDO::getAttribute' => ['', 'attribute'=>'int'], 'PDO::getAvailableDrivers' => ['array'], 'PDO::inTransaction' => ['bool'], -'PDO::lastInsertId' => ['string', 'seqname='=>'string'], +'PDO::lastInsertId' => ['string|false', 'seqname='=>'string'], 'PDO::pgsqlCopyFromArray' => ['bool', 'table_name'=>'string', 'rows'=>'array', 'delimiter'=>'string', 'null_as'=>'string', 'fields'=>'string'], 'PDO::pgsqlCopyFromFile' => ['bool', 'table_name'=>'string', 'filename'=>'string', 'delimiter'=>'string', 'null_as'=>'string', 'fields'=>'string'], 'PDO::pgsqlCopyToArray' => ['array', 'table_name'=>'string', 'delimiter'=>'string', 'null_as'=>'string', 'fields'=>'string'], 'PDO::pgsqlCopyToFile' => ['bool', 'table_name'=>'string', 'filename'=>'string', 'delimiter'=>'string', 'null_as'=>'string', 'fields'=>'string'], -'PDO::pgsqlGetNotify' => ['array', 'result_type'=>'int', 'ms_timeout'=>'int'], +'PDO::pgsqlGetNotify' => ['array', 'result_type='=>'int', 'ms_timeout='=>'int'], 'PDO::pgsqlGetPid' => ['int'], 'PDO::pgsqlLOBCreate' => ['string'], 'PDO::pgsqlLOBOpen' => ['resource', 'oid'=>'string', 'mode='=>'string'], 'PDO::pgsqlLOBUnlink' => ['bool', 'oid'=>'string'], -'PDO::prepare' => ['PDOStatement', 'statement'=>'string', 'options='=>'array'], +'PDO::prepare' => ['__benevolent', 'statement'=>'string', 'options='=>'array'], 'PDO::query' => ['PDOStatement|false', 'sql'=>'string'], 'PDO::query\'1' => ['PDOStatement|false', 'sql'=>'string', 'fetch_column'=>'int', 'colno'=>'int'], 'PDO::query\'2' => ['PDOStatement|false', 'sql'=>'string', 'fetch_class'=>'int', 'classname'=>'string', 'ctorargs'=>'array'], @@ -8471,7 +8465,7 @@ 'PDOException::getLine' => [''], 'PDOException::getMessage' => [''], 'PDOException::getPrevious' => [''], -'PDOException::getTrace' => [''], +'PDOException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'PDOException::getTraceAsString' => [''], 'PDOStatement::__sleep' => ['array'], 'PDOStatement::__wakeup' => ['void'], @@ -8495,7 +8489,7 @@ 'PDOStatement::setAttribute' => ['bool', 'attribute'=>'int', 'value'=>'mixed'], 'PDOStatement::setFetchMode' => ['bool', 'mode'=>'int'], 'PDOStatement::setFetchMode\'1' => ['bool', 'fetch_column'=>'int', 'colno'=>'int'], -'PDOStatement::setFetchMode\'2' => ['bool', 'fetch_class'=>'int', 'classname'=>'string', 'ctorargs'=>'array'], +'PDOStatement::setFetchMode\'2' => ['bool', 'fetch_class'=>'int', 'classname'=>'string', 'ctorargs='=>'?array'], 'PDOStatement::setFetchMode\'3' => ['bool', 'fetch_into'=>'int', 'object'=>'object'], 'pfsockopen' => ['resource|false', 'hostname'=>'string', 'port='=>'int', '&w_errno='=>'int', '&w_errstr='=>'string', 'timeout='=>'float'], 'pg_affected_rows' => ['int', 'result'=>'resource'], @@ -8535,7 +8529,7 @@ 'pg_fetch_row' => ['array|false', 'result'=>'resource', 'row='=>'?int', 'result_type='=>'int'], 'pg_field_is_null' => ['int|false', 'result'=>'', 'field_name_or_number'=>'string|int'], 'pg_field_is_null\'1' => ['int', 'result'=>'', 'row'=>'int', 'field_name_or_number'=>'string|int'], -'pg_field_name' => ['string', 'result'=>'resource', 'field_number'=>'int'], +'pg_field_name' => ['string|false', 'result'=>'resource', 'field_number'=>'int'], 'pg_field_num' => ['int', 'result'=>'resource', 'field_name'=>'string'], 'pg_field_prtlen' => ['int|false', 'result'=>'', 'field_name_or_number'=>''], 'pg_field_prtlen\'1' => ['int', 'result'=>'', 'row'=>'int', 'field_name_or_number'=>'string|int'], @@ -8613,7 +8607,7 @@ 'Phar::addFromString' => ['', 'localname'=>'string', 'contents'=>'string'], 'Phar::apiVersion' => ['string'], 'Phar::buildFromDirectory' => ['array', 'base_dir'=>'string', 'regex='=>'string'], -'Phar::buildFromIterator' => ['array', 'iter'=>'iterator', 'base_directory='=>'string'], +'Phar::buildFromIterator' => ['array', 'iter'=>'Iterator', 'base_directory='=>'string'], 'Phar::canCompress' => ['bool', 'method='=>'int'], 'Phar::canWrite' => ['bool'], 'Phar::compress' => ['Phar', 'compression'=>'int', 'extension='=>'string'], @@ -8629,7 +8623,7 @@ 'Phar::decompressFiles' => ['bool'], 'Phar::delete' => ['bool', 'entry'=>'string'], 'Phar::delMetadata' => ['bool'], -'Phar::extractTo' => ['bool', 'pathto'=>'string', 'files='=>'string|array', 'overwrite='=>'bool'], +'Phar::extractTo' => ['bool', 'pathto'=>'string', 'files='=>'string|array|null', 'overwrite='=>'bool'], 'Phar::getAlias' => ['string'], 'Phar::getMetadata' => ['mixed'], 'Phar::getModified' => ['bool'], @@ -8670,7 +8664,7 @@ 'PharData::addFile' => ['', 'file'=>'string', 'localname='=>'string'], 'PharData::addFromString' => ['bool', 'localname'=>'string', 'contents'=>'string'], 'PharData::buildFromDirectory' => ['array', 'base_dir'=>'string', 'regex='=>'string'], -'PharData::buildFromIterator' => ['array', 'iter'=>'iterator', 'base_directory='=>'string'], +'PharData::buildFromIterator' => ['array', 'iter'=>'Iterator', 'base_directory='=>'string'], 'PharData::compress' => ['PharData', 'compression'=>'int', 'extension='=>'string'], 'PharData::compressFiles' => ['bool', 'compression'=>'int'], 'PharData::convertToData' => ['PharData', 'format='=>'int', 'compression='=>'int', 'extension='=>'string'], @@ -8680,7 +8674,7 @@ 'PharData::decompressFiles' => ['bool'], 'PharData::delete' => ['bool', 'entry'=>'string'], 'PharData::delMetadata' => ['bool'], -'PharData::extractTo' => ['bool', 'pathto'=>'string', 'files='=>'string|array', 'overwrite='=>'bool'], +'PharData::extractTo' => ['bool', 'pathto'=>'string', 'files='=>'string|array|null', 'overwrite='=>'bool'], 'PharData::isWritable' => ['bool'], 'PharData::offsetGet' => ['PharFileInfo', 'offset'=>'string'], 'PharData::offsetSet' => ['', 'offset'=>'string', 'value'=>'string'], @@ -8742,7 +8736,8 @@ 'phpdbg_prompt' => ['', 'prompt'=>'string'], 'phpdbg_start_oplog' => [''], 'phpinfo' => ['bool', 'what='=>'int'], -'phpversion' => ['string|false', 'extension='=>'string'], +'phpversion' => ['string'], +'phpversion\'1' => ['string|false', 'extension'=>'string'], 'pht\AtomicInteger::__construct' => ['void', 'value='=>'int'], 'pht\AtomicInteger::dec' => ['void'], 'pht\AtomicInteger::get' => ['int'], @@ -8831,18 +8826,17 @@ 'Postal\Expand::expand_address' => ['string[]', 'address'=>'string', 'options='=>'array'], 'Postal\Parser::parse_address' => ['array', 'address'=>'string', 'options='=>'array'], 'pow' => ['float|int', 'base'=>'int|float', 'exponent'=>'int|float'], -'preg_filter' => ['mixed', 'regex'=>'mixed', 'replace'=>'mixed', 'subject'=>'mixed', 'limit='=>'int', '&w_count='=>'int'], +'preg_filter' => ['string|array|null', 'regex'=>'string|array', 'replace'=>'string|array', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], 'preg_grep' => ['array|false', 'regex'=>'string', 'input'=>'array', 'flags='=>'int'], 'preg_last_error' => ['int'], -'preg_match' => ['int|false', 'pattern'=>'string', 'subject'=>'string', '&w_subpatterns='=>'string[]', 'flags='=>'int', 'offset='=>'int'], -'preg_match_all' => ['int|false', 'pattern'=>'string', 'subject'=>'string', '&w_subpatterns='=>'array', 'flags='=>'int', 'offset='=>'int'], +'preg_match' => ['0|1|false', 'pattern'=>'string', 'subject'=>'string', '&w_subpatterns='=>'string[]', 'flags='=>'int', 'offset='=>'int'], +'preg_match_all' => ['0|positive-int|false|null', 'pattern'=>'string', 'subject'=>'string', '&w_subpatterns='=>'array', 'flags='=>'int', 'offset='=>'int'], 'preg_quote' => ['string', 'str'=>'string', 'delim_char='=>'string'], 'preg_replace' => ['string|array|null', 'regex'=>'string|array', 'replace'=>'string|array', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], -'preg_replace_callback' => ['string|array|null', 'regex'=>'string|array', 'callback'=>'callable', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], +'preg_replace_callback' => ['string|array|null', 'regex'=>'string|array', 'callback'=>'callable(array):string', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], 'preg_replace_callback_array' => ['string|array|null', 'pattern'=>'array', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int'], 'preg_split' => ['array|false', 'pattern'=>'string', 'subject'=>'string', 'limit='=>'?int', 'flags='=>'int'], -'prev' => ['mixed', '&rw_array_arg'=>'array'], -'print' => ['int', 'arg'=>'string'], +'prev' => ['mixed', '&rw_array_arg'=>'array|object'], 'print_r' => ['string|true', 'var'=>'mixed', 'return='=>'bool'], 'printf' => ['int', 'format'=>'string', '...values='=>'string|int|float'], 'proc_close' => ['int', 'process'=>'resource'], @@ -9062,7 +9056,7 @@ 'radius_strerror' => ['string', 'radius_handle'=>'resource'], 'rand' => ['int', 'min'=>'int', 'max'=>'int'], 'rand\'1' => ['int'], -'random_bytes' => ['string', 'length'=>'int'], +'random_bytes' => ['non-empty-string', 'length'=>'positive-int'], 'random_int' => ['int', 'min'=>'int', 'max'=>'int'], 'range' => ['array', 'low'=>'int|float|string', 'high'=>'int|float|string', 'step='=>'int|float'], 'RangeException::__clone' => ['void'], @@ -9073,7 +9067,7 @@ 'RangeException::getLine' => ['int'], 'RangeException::getMessage' => ['string'], 'RangeException::getPrevious' => ['Throwable|RangeException|null'], -'RangeException::getTrace' => ['array'], +'RangeException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'RangeException::getTraceAsString' => ['string'], 'rar_allow_broken_set' => ['bool', 'rarfile'=>'RarArchive', 'allow_broken'=>'bool'], 'rar_broken_is' => ['bool', 'rarfile'=>'RarArchive'], @@ -9116,7 +9110,7 @@ 'RarException::getLine' => ['int'], 'RarException::getMessage' => ['string'], 'RarException::getPrevious' => ['Exception|Throwable'], -'RarException::getTrace' => ['array'], +'RarException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'RarException::getTraceAsString' => ['string'], 'RarException::isUsingExceptions' => ['bool'], 'RarException::setUsingExceptions' => ['void', 'using_exceptions'=>'bool'], @@ -9124,8 +9118,8 @@ 'rawurlencode' => ['string', 'str'=>'string'], 'read_exif_data' => ['array', 'filename'=>'string|resource', 'sections_needed='=>'string', 'sub_arrays='=>'bool', 'read_thumbnail='=>'bool'], 'readdir' => ['string|false', 'dir_handle='=>'resource'], -'readfile' => ['int|false', 'filename'=>'string', 'use_include_path='=>'bool', 'context='=>'resource'], -'readgzfile' => ['int|false', 'filename'=>'string', 'use_include_path='=>'int'], +'readfile' => ['0|positive-int|false', 'filename'=>'string', 'use_include_path='=>'bool', 'context='=>'resource'], +'readgzfile' => ['0|positive-int|false', 'filename'=>'string', 'use_include_path='=>'int'], 'readline' => ['string|false', 'prompt='=>'?string'], 'readline_add_history' => ['bool', 'prompt'=>'string'], 'readline_callback_handler_install' => ['bool', 'prompt'=>'string', 'callback'=>'callable'], @@ -9175,14 +9169,14 @@ 'RecursiveArrayIterator::seek' => ['void', 'position'=>'int'], 'RecursiveArrayIterator::serialize' => ['string'], 'RecursiveArrayIterator::setFlags' => ['void', 'flags'=>'string'], -'RecursiveArrayIterator::uasort' => ['void', 'cmp_function'=>'callable(mixed,mixed):int'], -'RecursiveArrayIterator::uksort' => ['void', 'cmp_function'=>'callable(array-key,array-key):int'], +'RecursiveArrayIterator::uasort' => ['void', 'callback'=>'callable(mixed,mixed):int'], +'RecursiveArrayIterator::uksort' => ['void', 'callback'=>'callable(array-key,array-key):int'], 'RecursiveArrayIterator::unserialize' => ['string', 'serialized'=>'string'], 'RecursiveArrayIterator::valid' => ['bool'], 'RecursiveCachingIterator::__construct' => ['void', 'iterator'=>'Iterator', 'flags'=>''], 'RecursiveCachingIterator::getChildren' => ['RecursiveCachingIterator'], 'RecursiveCachingIterator::hasChildren' => ['bool'], -'RecursiveCallbackFilterIterator::__construct' => ['void', 'iterator'=>'recursiveiterator', 'func'=>'callable'], +'RecursiveCallbackFilterIterator::__construct' => ['void', 'iterator'=>'RecursiveIterator', 'func'=>'callable'], 'RecursiveCallbackFilterIterator::getChildren' => ['RecursiveCallbackFilterIterator'], 'RecursiveCallbackFilterIterator::hasChildren' => ['void'], 'RecursiveDirectoryIterator::__construct' => ['void', 'path'=>'string', 'flags='=>'int'], @@ -9193,7 +9187,7 @@ 'RecursiveDirectoryIterator::key' => ['string'], 'RecursiveDirectoryIterator::next' => ['void'], 'RecursiveDirectoryIterator::rewind' => ['void'], -'RecursiveFilterIterator::__construct' => ['void', 'iterator'=>'recursiveiterator'], +'RecursiveFilterIterator::__construct' => ['void', 'iterator'=>'RecursiveIterator'], 'RecursiveFilterIterator::getChildren' => ['RecursiveFilterIterator'], 'RecursiveFilterIterator::hasChildren' => ['bool'], 'RecursiveIterator::getChildren' => ['RecursiveIterator'], @@ -9216,10 +9210,10 @@ 'RecursiveIteratorIterator::rewind' => ['void'], 'RecursiveIteratorIterator::setMaxDepth' => ['void', 'max_depth='=>'int'], 'RecursiveIteratorIterator::valid' => ['bool'], -'RecursiveRegexIterator::__construct' => ['void', 'iterator'=>'recursiveiterator', 'regex='=>'string', 'mode='=>'int', 'flags='=>'int', 'preg_flags='=>'int'], +'RecursiveRegexIterator::__construct' => ['void', 'iterator'=>'RecursiveIterator', 'regex='=>'string', 'mode='=>'int', 'flags='=>'int', 'preg_flags='=>'int'], 'RecursiveRegexIterator::getChildren' => ['RecursiveRegexIterator'], 'RecursiveRegexIterator::hasChildren' => ['bool'], -'RecursiveTreeIterator::__construct' => ['void', 'iterator'=>'recursiveiterator|iteratoraggregate', 'flags='=>'int', 'cit_flags='=>'int', 'mode='=>'int'], +'RecursiveTreeIterator::__construct' => ['void', 'iterator'=>'RecursiveIterator|IteratorAggregate', 'flags='=>'int', 'cit_flags='=>'int', 'mode='=>'int'], 'RecursiveTreeIterator::beginChildren' => ['void'], 'RecursiveTreeIterator::beginIteration' => ['RecursiveIterator'], 'RecursiveTreeIterator::callGetChildren' => ['RecursiveIterator'], @@ -9401,7 +9395,7 @@ 'Redis::sRem' => ['int', 'key'=>'string', 'member1'=>'string', '...other_members='=>'string'], 'Redis::sRemove' => ['int', 'key'=>'string', 'member1'=>'string', '...other_members='=>'string'], 'Redis::sScan' => ['array|bool', 'key'=>'string', '&iterator'=>'int', 'pattern='=>'string', 'count='=>'int'], -'Redis::strLen' => ['int', 'key'=>'string'], +'Redis::strLen' => ['0|positive-int', 'key'=>'string'], 'Redis::subscribe' => ['mixed|null', 'channels'=>'array', 'callback'=>'string|array'], 'Redis::substr' => ['', 'key'=>'string', 'start'=>'int', 'end'=>'int'], 'Redis::sUnion' => ['array', 'key'=>'string', '...other_keys='=>'string'], @@ -9591,7 +9585,7 @@ 'RedisCluster::sRandMember' => ['array|string', 'key'=>'string', 'count='=>'int'], 'RedisCluster::sRem' => ['int', 'key'=>'string', 'member1'=>'string', '...other_members='=>'string'], 'RedisCluster::sScan' => ['array', 'key'=>'string', '&iterator'=>'int', 'pattern='=>'null', 'count='=>'int'], -'RedisCluster::strlen' => ['int', 'key'=>'string'], +'RedisCluster::strlen' => ['0|positive-int', 'key'=>'string'], 'RedisCluster::subscribe' => ['mixed', 'channels'=>'array', 'callback'=>'string'], 'RedisCluster::sUnion' => ['array', 'key1'=>'string', '...other_keys='=>'string'], 'RedisCluster::sUnionStore' => ['int', 'dstKey'=>'string', 'key1'=>'string', '...other_keys='=>'string'], @@ -9673,7 +9667,7 @@ 'ReflectionClass::hasConstant' => ['bool', 'name'=>'string'], 'ReflectionClass::hasMethod' => ['bool', 'name'=>'string'], 'ReflectionClass::hasProperty' => ['bool', 'name'=>'string'], -'ReflectionClass::implementsInterface' => ['bool', 'interface_name'=>'string|reflectionclass'], +'ReflectionClass::implementsInterface' => ['bool', 'interface_name'=>'string|ReflectionClass'], 'ReflectionClass::inNamespace' => ['bool'], 'ReflectionClass::isAbstract' => ['bool'], 'ReflectionClass::isAnonymous' => ['bool'], @@ -9685,13 +9679,13 @@ 'ReflectionClass::isInternal' => ['bool'], 'ReflectionClass::isIterable' => ['bool'], 'ReflectionClass::isIterateable' => ['bool'], -'ReflectionClass::isSubclassOf' => ['bool', 'class'=>'string|reflectionclass'], +'ReflectionClass::isSubclassOf' => ['bool', 'class'=>'string|ReflectionClass'], 'ReflectionClass::isTrait' => ['bool'], 'ReflectionClass::isUserDefined' => ['bool'], 'ReflectionClass::newInstance' => ['object', 'args='=>'mixed', '...args='=>'mixed'], 'ReflectionClass::newInstanceArgs' => ['object', 'args='=>'array'], 'ReflectionClass::newInstanceWithoutConstructor' => ['object'], -'ReflectionClass::setStaticPropertyValue' => ['void', 'name'=>'string', 'value'=>'string'], +'ReflectionClass::setStaticPropertyValue' => ['void', 'name'=>'string', 'value'=>'mixed'], 'ReflectionClassConstant::__construct' => ['void', 'class'=>'mixed', 'name'=>'string'], 'ReflectionClassConstant::__toString' => ['string'], 'ReflectionClassConstant::export' => ['string', 'class'=>'mixed', 'name'=>'string', 'return='=>'bool'], @@ -9782,7 +9776,7 @@ 'ReflectionGenerator::getExecutingLine' => ['int'], 'ReflectionGenerator::getFunction' => ['ReflectionFunctionAbstract'], 'ReflectionGenerator::getThis' => ['object'], -'ReflectionGenerator::getTrace' => ['array', 'options'=>'int'], +'ReflectionGenerator::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}', 'options'=>'int'], 'ReflectionMethod::__construct' => ['void', 'class'=>'string|object', 'name'=>'string'], 'ReflectionMethod::__construct\'1' => ['void', 'class_method'=>'string'], 'ReflectionMethod::__toString' => ['string'], @@ -9861,7 +9855,7 @@ 'ReflectionZendExtension::getVersion' => ['string'], 'Reflector::__toString' => ['string'], 'Reflector::export' => ['?string'], -'RegexIterator::__construct' => ['void', 'iterator'=>'iterator', 'regex'=>'string', 'mode='=>'int', 'flags='=>'int', 'preg_flags='=>'int'], +'RegexIterator::__construct' => ['void', 'iterator'=>'Iterator', 'regex'=>'string', 'mode='=>'int', 'flags='=>'int', 'preg_flags='=>'int'], 'RegexIterator::accept' => ['bool'], 'RegexIterator::getFlags' => ['int'], 'RegexIterator::getMode' => ['int'], @@ -9875,7 +9869,7 @@ 'register_tick_function' => ['bool', 'function'=>'callable(): void', '...args='=>'mixed'], 'rename' => ['bool', 'old_name'=>'string', 'new_name'=>'string', 'context='=>'resource'], 'rename_function' => ['bool', 'original_name'=>'string', 'new_name'=>'string'], -'reset' => ['mixed', '&rw_array'=>'array'], +'reset' => ['mixed', '&rw_array'=>'array|object'], 'ResourceBundle::__construct' => ['void', 'locale'=>'string', 'bundlename'=>'string', 'fallback='=>'bool'], 'ResourceBundle::count' => ['0|positive-int'], 'ResourceBundle::create' => ['?ResourceBundle', 'locale'=>'string', 'bundlename'=>'string', 'fallback='=>'bool'], @@ -9889,13 +9883,13 @@ 'resourcebundle_get_error_code' => ['int', 'r'=>'resourcebundle'], 'resourcebundle_get_error_message' => ['string', 'r'=>'resourcebundle'], 'resourcebundle_locales' => ['array|false', 'bundlename'=>'string'], -'restore_error_handler' => ['bool'], -'restore_exception_handler' => ['bool'], +'restore_error_handler' => ['true'], +'restore_exception_handler' => ['true'], 'restore_include_path' => ['void'], 'rewind' => ['bool', 'fp'=>'resource'], 'rewinddir' => ['null|false', 'dir_handle='=>'resource'], 'rmdir' => ['bool', 'dirname'=>'string', 'context='=>'resource'], -'round' => ['float', 'number'=>'float', 'precision='=>'int', 'mode='=>'int'], +'round' => ['float|false', 'number'=>'float', 'precision='=>'int', 'mode='=>'int'], 'rpm_close' => ['bool', 'rpmr'=>'resource'], 'rpm_get_tag' => ['mixed', 'rpmr'=>'resource', 'tagnum'=>'int'], 'rpm_is_valid' => ['bool', 'filename'=>'string'], @@ -9963,7 +9957,7 @@ 'RuntimeException::getLine' => ['int'], 'RuntimeException::getMessage' => ['string'], 'RuntimeException::getPrevious' => ['Throwable|RuntimeException|null'], -'RuntimeException::getTrace' => ['array'], +'RuntimeException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'RuntimeException::getTraceAsString' => ['string'], 'SAMConnection::commit' => ['bool'], 'SAMConnection::connect' => ['bool', 'protocol'=>'string', 'properties='=>'array'], @@ -10124,7 +10118,7 @@ 'SessionHandler::close' => ['bool'], 'SessionHandler::create_sid' => ['char'], 'SessionHandler::destroy' => ['bool', 'id'=>'string'], -'SessionHandler::gc' => ['bool', 'maxlifetime'=>'int'], +'SessionHandler::gc' => ['int|false', 'maxlifetime'=>'int'], 'SessionHandler::open' => ['bool', 'save_path'=>'string', 'session_name'=>'string'], 'SessionHandler::read' => ['string', 'id'=>'string'], 'SessionHandler::updateTimestamp' => ['bool', 'session_id'=>'string', 'session_data'=>'string'], @@ -10132,7 +10126,7 @@ 'SessionHandler::write' => ['bool', 'id'=>'string', 'data'=>'string'], 'SessionHandlerInterface::close' => ['bool'], 'SessionHandlerInterface::destroy' => ['bool', 'session_id'=>'string'], -'SessionHandlerInterface::gc' => ['bool', 'maxlifetime'=>'int'], +'SessionHandlerInterface::gc' => ['int|false', 'maxlifetime'=>'int'], 'SessionHandlerInterface::open' => ['bool', 'save_path'=>'string', 'name'=>'string'], 'SessionHandlerInterface::read' => ['string', 'session_id'=>'string'], 'SessionHandlerInterface::write' => ['bool', 'session_id'=>'string', 'session_data'=>'string'], @@ -10141,7 +10135,7 @@ 'SessionUpdateTimestampHandler::validateId' => ['char', 'id'=>'string'], 'SessionUpdateTimestampHandlerInterface::updateTimestamp' => ['bool', 'key'=>'string', 'val'=>'string'], 'SessionUpdateTimestampHandlerInterface::validateId' => ['bool', 'key'=>'string'], -'set_error_handler' => ['?callable', 'callback'=>'null|callable(int,string,string,int,array):bool|callable(int,string,string,int):bool|callable(int,string,string):bool|callable(int,string):bool', 'error_types='=>'int'], +'set_error_handler' => ['?callable', 'callback'=>'null|callable(int,string,string,int,array):bool', 'error_types='=>'int'], 'set_exception_handler' => ['null|callable(Throwable):void', 'exception_handler'=>'null|callable(Throwable):void'], 'set_file_buffer' => ['int', 'fp'=>'resource', 'buffer'=>'int'], 'set_include_path' => ['string|false', 'new_include_path'=>'string'], @@ -10284,20 +10278,20 @@ 'snmpset' => ['bool', 'host'=>'string', 'community'=>'string', 'object_id'=>'string', 'type'=>'string', 'value'=>'mixed', 'timeout='=>'int', 'retries='=>'int'], 'snmpwalk' => ['array|false', 'host'=>'string', 'community'=>'string', 'object_id'=>'string', 'timeout='=>'int', 'retries='=>'int'], 'snmpwalkoid' => ['array|false', 'hostname'=>'string', 'community'=>'string', 'object_id'=>'string', 'timeout='=>'int', 'retries='=>'int'], -'SoapClient::__call' => ['', 'function_name'=>'string', 'arguments'=>'array'], +'SoapClient::__call' => ['mixed', 'function_name'=>'string', 'arguments'=>'array'], 'SoapClient::__construct' => ['void', 'wsdl'=>'mixed', 'options='=>'array|null'], -'SoapClient::__doRequest' => ['string', 'request'=>'string', 'location'=>'string', 'action'=>'string', 'version'=>'int', 'one_way='=>'int'], +'SoapClient::__doRequest' => ['string|null', 'request'=>'string', 'location'=>'string', 'action'=>'string', 'version'=>'int', 'one_way='=>'int'], 'SoapClient::__getCookies' => ['array'], -'SoapClient::__getFunctions' => ['array'], -'SoapClient::__getLastRequest' => ['string'], -'SoapClient::__getLastRequestHeaders' => ['string'], -'SoapClient::__getLastResponse' => ['string'], -'SoapClient::__getLastResponseHeaders' => ['string'], -'SoapClient::__getTypes' => ['array'], +'SoapClient::__getFunctions' => ['array|null'], +'SoapClient::__getLastRequest' => ['string|null'], +'SoapClient::__getLastRequestHeaders' => ['string|null'], +'SoapClient::__getLastResponse' => ['string|null'], +'SoapClient::__getLastResponseHeaders' => ['string|null'], +'SoapClient::__getTypes' => ['array|null'], 'SoapClient::__setCookie' => ['', 'name'=>'string', 'value='=>'string'], -'SoapClient::__setLocation' => ['string', 'new_location='=>'string'], +'SoapClient::__setLocation' => ['string|null', 'new_location='=>'string'], 'SoapClient::__setSoapHeaders' => ['bool', 'soapheaders='=>''], -'SoapClient::__soapCall' => ['', 'function_name'=>'string', 'arguments'=>'array', 'options='=>'array', 'input_headers='=>'SoapHeader|array', '&w_output_headers='=>'array'], +'SoapClient::__soapCall' => ['mixed', 'function_name'=>'string', 'arguments'=>'array', 'options='=>'array', 'input_headers='=>'SoapHeader|array', '&w_output_headers='=>'array'], 'SoapClient::SoapClient' => ['object', 'wsdl'=>'mixed', 'options='=>'array|null'], 'SoapFault::__construct' => ['void', 'faultcode'=>'string', 'string'=>'string', 'faultactor='=>'string', 'detail='=>'string', 'faultname='=>'string', 'headerfault='=>'string'], 'SoapFault::__toString' => ['string'], @@ -10417,7 +10411,7 @@ 'Sodium\randombytes_uniform' => ['int', 'upperBoundNonInclusive'=>'int'], 'Sodium\version_string' => ['string'], 'sodium_add' => ['string', 'string_1'=>'string', 'string_2'=>'string'], -'sodium_base642bin' => ['string', 'base64'=>'string', 'variant'=>'int', 'ignore'=>'string'], +'sodium_base642bin' => ['string', 'base64'=>'string', 'variant'=>'int', 'ignore='=>'string'], 'sodium_bin2base64' => ['string', 'binary'=>'string', 'variant'=>'int'], 'sodium_bin2hex' => ['string', 'binary'=>'string'], 'sodium_compare' => ['int', 'string_1'=>'string', 'string_2'=>'string'], @@ -10820,7 +10814,7 @@ 'SolrException::getLine' => ['int'], 'SolrException::getMessage' => ['string'], 'SolrException::getPrevious' => ['Exception|Throwable'], -'SolrException::getTrace' => ['array'], +'SolrException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'SolrException::getTraceAsString' => ['string'], 'SolrGenericResponse::__construct' => ['void'], 'SolrGenericResponse::__destruct' => [''], @@ -10845,7 +10839,7 @@ 'SolrIllegalArgumentException::getLine' => ['int'], 'SolrIllegalArgumentException::getMessage' => ['string'], 'SolrIllegalArgumentException::getPrevious' => ['Exception|Throwable'], -'SolrIllegalArgumentException::getTrace' => ['array'], +'SolrIllegalArgumentException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'SolrIllegalArgumentException::getTraceAsString' => ['string'], 'SolrIllegalOperationException::__clone' => ['void'], 'SolrIllegalOperationException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Exception)|(?Throwable)'], @@ -10857,7 +10851,7 @@ 'SolrIllegalOperationException::getLine' => ['int'], 'SolrIllegalOperationException::getMessage' => ['string'], 'SolrIllegalOperationException::getPrevious' => ['Exception|Throwable'], -'SolrIllegalOperationException::getTrace' => ['array'], +'SolrIllegalOperationException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'SolrIllegalOperationException::getTraceAsString' => ['string'], 'SolrInputDocument::__clone' => ['void'], 'SolrInputDocument::__construct' => ['void'], @@ -11165,7 +11159,7 @@ 'SolrServerException::getLine' => ['int'], 'SolrServerException::getMessage' => ['string'], 'SolrServerException::getPrevious' => ['Exception|Throwable'], -'SolrServerException::getTrace' => ['array'], +'SolrServerException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'SolrServerException::getTraceAsString' => ['string'], 'SolrUpdateResponse::__construct' => ['void'], 'SolrUpdateResponse::__destruct' => [''], @@ -11257,24 +11251,24 @@ 'SplEnum::getConstList' => ['array', 'include_default='=>'bool'], 'SplFileInfo::__construct' => ['void', 'file_name'=>'string'], 'SplFileInfo::__toString' => ['string'], -'SplFileInfo::getATime' => ['int'], +'SplFileInfo::getATime' => ['__benevolent'], 'SplFileInfo::getBasename' => ['string', 'suffix='=>'string'], 'SplFileInfo::getCTime' => ['int'], 'SplFileInfo::getExtension' => ['string'], 'SplFileInfo::getFileInfo' => ['SplFileInfo', 'class_name='=>'string'], 'SplFileInfo::getFilename' => ['string'], -'SplFileInfo::getGroup' => ['int'], -'SplFileInfo::getInode' => ['int'], -'SplFileInfo::getLinkTarget' => ['string'], -'SplFileInfo::getMTime' => ['int'], -'SplFileInfo::getOwner' => ['int'], +'SplFileInfo::getGroup' => ['__benevolent'], +'SplFileInfo::getInode' => ['__benevolent'], +'SplFileInfo::getLinkTarget' => ['__benevolent'], +'SplFileInfo::getMTime' => ['__benevolent'], +'SplFileInfo::getOwner' => ['__benevolent'], 'SplFileInfo::getPath' => ['string'], 'SplFileInfo::getPathInfo' => ['SplFileInfo', 'class_name='=>'string'], 'SplFileInfo::getPathname' => ['string'], -'SplFileInfo::getPerms' => ['int'], -'SplFileInfo::getRealPath' => ['string|false'], -'SplFileInfo::getSize' => ['int'], -'SplFileInfo::getType' => ['string'], +'SplFileInfo::getPerms' => ['__benevolent'], +'SplFileInfo::getRealPath' => ['__benevolent'], +'SplFileInfo::getSize' => ['__benevolent'], +'SplFileInfo::getType' => ['__benevolent'], 'SplFileInfo::isDir' => ['bool'], 'SplFileInfo::isExecutable' => ['bool'], 'SplFileInfo::isFile' => ['bool'], @@ -11300,7 +11294,7 @@ 'SplFileObject::fread' => ['string|false', 'length'=>'int'], 'SplFileObject::fscanf' => ['bool', 'format'=>'string', '&...w_vars='=>'string|int|float'], 'SplFileObject::fseek' => ['int', 'pos'=>'int', 'whence='=>'int'], -'SplFileObject::fstat' => ['array|false'], +'SplFileObject::fstat' => ['array'], 'SplFileObject::ftell' => ['int|false'], 'SplFileObject::ftruncate' => ['bool', 'size'=>'int'], 'SplFileObject::fwrite' => ['int', 'str'=>'string', 'length='=>'int'], @@ -11351,7 +11345,7 @@ 'spliti' => ['array', 'pattern'=>'string', 'string'=>'string', 'limit='=>'int'], 'SplMaxHeap::compare' => ['int', 'a'=>'mixed', 'b'=>'mixed'], 'SplMinHeap::compare' => ['int', 'a'=>'mixed', 'b'=>'mixed'], -'SplObjectStorage::addAll' => ['void', 'os'=>'splobjectstorage'], +'SplObjectStorage::addAll' => ['void', 'os'=>'SplObjectStorage'], 'SplObjectStorage::attach' => ['void', 'obj'=>'object', 'inf='=>'mixed'], 'SplObjectStorage::contains' => ['bool', 'obj'=>'object'], 'SplObjectStorage::count' => ['0|positive-int'], @@ -11365,14 +11359,14 @@ 'SplObjectStorage::offsetGet' => ['mixed', 'obj'=>'object'], 'SplObjectStorage::offsetSet' => ['object', 'object'=>'object', 'data='=>'mixed'], 'SplObjectStorage::offsetUnset' => ['object', 'object'=>'object'], -'SplObjectStorage::removeAll' => ['void', 'os'=>'splobjectstorage'], -'SplObjectStorage::removeAllExcept' => ['void', 'os'=>'splobjectstorage'], +'SplObjectStorage::removeAll' => ['void', 'os'=>'SplObjectStorage'], +'SplObjectStorage::removeAllExcept' => ['void', 'os'=>'SplObjectStorage'], 'SplObjectStorage::rewind' => ['void'], 'SplObjectStorage::serialize' => ['string'], 'SplObjectStorage::setInfo' => ['void', 'inf'=>'mixed'], 'SplObjectStorage::unserialize' => ['void', 'serialized'=>'string'], 'SplObjectStorage::valid' => ['bool'], -'SplObserver::update' => ['void', 'subject'=>'splsubject'], +'SplObserver::update' => ['void', 'subject'=>'SplSubject'], 'SplPriorityQueue::compare' => ['int', 'a'=>'mixed', 'b'=>'mixed'], 'SplPriorityQueue::count' => ['0|positive-int'], 'SplPriorityQueue::current' => ['mixed'], @@ -11391,8 +11385,8 @@ 'SplQueue::enqueue' => ['void', 'value'=>'mixed'], 'SplQueue::setIteratorMode' => ['void', 'mode'=>'int'], 'SplStack::setIteratorMode' => ['void', 'mode'=>'int'], -'SplSubject::attach' => ['void', 'observer'=>'splobserver'], -'SplSubject::detach' => ['void', 'observer'=>'splobserver'], +'SplSubject::attach' => ['void', 'observer'=>'SplObserver'], +'SplSubject::detach' => ['void', 'observer'=>'SplObserver'], 'SplSubject::notify' => ['void'], 'SplTempFileObject::__construct' => ['void', 'max_memory='=>'int'], 'SplType::__construct' => ['void', 'initial_value='=>'mixed', 'strict='=>'bool'], @@ -11546,7 +11540,8 @@ 'sqlsrv_server_info' => ['array', 'conn'=>'resource'], 'sqrt' => ['float', 'number'=>'float'], 'srand' => ['void', 'seed='=>'int', 'mode='=>'int'], -'sscanf' => ['mixed', 'str'=>'string', 'format'=>'string', '&...w_vars='=>'string|int|float|null'], +'sscanf' => ['int|null', 'str'=>'string', 'format'=>'string', '&w_war'=>'string|int|float|null', '&...w_vars='=>'string|int|float|null'], +'sscanf\'1' => ['array|null', 'str'=>'string', 'format'=>'string'], 'ssdeep_fuzzy_compare' => ['int', 'signature1'=>'string', 'signature2'=>'string'], 'ssdeep_fuzzy_hash' => ['string', 'to_hash'=>'string'], 'ssdeep_fuzzy_hash_filename' => ['string', 'file_name'=>'string'], @@ -11696,7 +11691,7 @@ 'str_replace' => ['string|array', 'search'=>'string|array', 'replace'=>'string|array', 'subject'=>'string|array', '&w_replace_count='=>'int'], 'str_rot13' => ['string', 'str'=>'string'], 'str_shuffle' => ['string', 'str'=>'string'], -'str_split' => ['array|false', 'str'=>'string', 'split_length='=>'int'], +'str_split' => ['non-empty-array|false', 'str'=>'string', 'split_length='=>'positive-int'], 'str_word_count' => ['array|int|false', 'string'=>'string', 'format='=>'int', 'charlist='=>'string'], 'strcasecmp' => ['int', 'str1'=>'string', 'str2'=>'string'], 'strchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'], @@ -11705,7 +11700,7 @@ 'strcspn' => ['int', 'str'=>'string', 'mask'=>'string', 'start='=>'int', 'length='=>'int'], 'stream_bucket_append' => ['void', 'brigade'=>'resource', 'bucket'=>'object'], 'stream_bucket_make_writeable' => ['object|null', 'brigade'=>'resource'], -'stream_bucket_new' => ['resource', 'stream'=>'resource', 'buffer'=>'string'], +'stream_bucket_new' => ['object', 'stream'=>'resource', 'buffer'=>'string'], 'stream_bucket_prepend' => ['void', 'brigade'=>'resource', 'bucket'=>'object'], 'stream_context_create' => ['resource', 'options='=>'array', 'params='=>'array'], 'stream_context_get_default' => ['resource', 'options='=>'array'], @@ -11731,7 +11726,7 @@ 'stream_isatty' => ['bool', 'stream'=>'resource'], 'stream_notification_callback' => ['callback', 'notification_code'=>'int', 'severity'=>'int', 'message'=>'string', 'message_code'=>'int', 'bytes_transferred'=>'int', 'bytes_max'=>'int'], 'stream_resolve_include_path' => ['string|false', 'filename'=>'string'], -'stream_select' => ['int|false', '&rw_read_streams'=>'resource[]', '&rw_write_streams'=>'resource[]|null', '&rw_except_streams'=>'resource[]|null', 'tv_sec'=>'?int', 'tv_usec='=>'?int'], +'stream_select' => ['int|false', '&rw_read_streams'=>'resource[]|null', '&rw_write_streams'=>'resource[]|null', '&rw_except_streams'=>'resource[]|null', 'tv_sec'=>'?int', 'tv_usec='=>'?int'], 'stream_set_blocking' => ['bool', 'socket'=>'resource', 'mode'=>'bool'], 'stream_set_chunk_size' => ['int|false', 'fp'=>'resource', 'chunk_size'=>'int'], 'stream_set_read_buffer' => ['int', 'fp'=>'resource', 'buffer'=>'int'], @@ -11803,9 +11798,9 @@ 'strtr' => ['string', 'str'=>'string', 'from'=>'string', 'to'=>'string'], 'strtr\'1' => ['string', 'str'=>'string', 'replace_pairs'=>'array'], 'strval' => ['string', 'var'=>'mixed'], -'substr' => ['string', 'string'=>'string', 'start'=>'int', 'length='=>'int'], +'substr' => ['__benevolent', 'string'=>'string', 'start'=>'int', 'length='=>'int'], 'substr_compare' => ['int|false', 'main_str'=>'string', 'str'=>'string', 'offset'=>'int', 'length='=>'int', 'case_sensitivity='=>'bool'], -'substr_count' => ['int', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'length='=>'int'], +'substr_count' => ['0|positive-int', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'length='=>'int'], 'substr_replace' => ['string|array', 'str'=>'string|array', 'repl'=>'mixed', 'start'=>'mixed', 'length='=>'mixed'], 'suhosin_encrypt_cookie' => ['string', 'name'=>'string', 'value'=>'string'], 'suhosin_get_raw_cookies' => ['array'], @@ -12243,7 +12238,7 @@ 'Throwable::getLine' => ['int'], 'Throwable::getMessage' => ['string'], 'Throwable::getPrevious' => ['Throwable|null'], -'Throwable::getTrace' => ['array'], +'Throwable::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'Throwable::getTraceAsString' => ['string'], 'tidy::__construct' => ['void', 'filename='=>'string', 'config='=>'', 'encoding='=>'string', 'use_include_path='=>'bool'], 'tidy::body' => ['tidyNode'], @@ -12304,12 +12299,12 @@ 'tidyNode::isJste' => ['bool'], 'tidyNode::isPhp' => ['bool'], 'tidyNode::isText' => ['bool'], -'time' => ['int'], -'time_nanosleep' => ['array{0:int,1:int}|bool', 'seconds'=>'int', 'nanoseconds'=>'int'], +'time' => ['positive-int'], +'time_nanosleep' => ['array{0:0|positive-int,1:0|positive-int}|bool', 'seconds'=>'int', 'nanoseconds'=>'int'], 'time_sleep_until' => ['bool', 'timestamp'=>'float'], 'timezone_abbreviations_list' => ['array'], -'timezone_identifiers_list' => ['array', 'what='=>'int', 'country='=>'?string'], -'timezone_location_get' => ['array|false', 'object'=>'DateTimeZone'], +'timezone_identifiers_list' => ['array', 'what='=>'int', 'country='=>'?string'], +'timezone_location_get' => ['array{country_code: string, latitude: float, longitude: float, comments: string}|false', 'object'=>'DateTimeZone'], 'timezone_name_from_abbr' => ['string|false', 'abbr'=>'string', 'gmtoffset='=>'int', 'isdst='=>'int'], 'timezone_name_get' => ['string', 'object'=>'DateTimeZone'], 'timezone_offset_get' => ['int', 'object'=>'DateTimeZone', 'datetime'=>'DateTime'], @@ -12563,9 +12558,9 @@ 'TypeError::getLine' => ['int'], 'TypeError::getMessage' => ['string'], 'TypeError::getPrevious' => ['Throwable|TypeError|null'], -'TypeError::getTrace' => ['array'], +'TypeError::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'TypeError::getTraceAsString' => ['string'], -'uasort' => ['bool', '&rw_array_arg'=>'array', 'cmp_function'=>'callable(mixed,mixed):int'], +'uasort' => ['bool', '&rw_array_arg'=>'array', 'callback'=>'callable(mixed,mixed):int'], 'ucfirst' => ['string', 'str'=>'string'], 'UConverter::__construct' => ['void', 'destination_encoding'=>'string', 'source_encoding='=>'string'], 'UConverter::convert' => ['string', 'str'=>'string', 'reverse='=>'bool'], @@ -12614,7 +12609,7 @@ 'ui\draw\text\font\fontfamilies' => ['array'], 'ui\quit' => ['void'], 'ui\run' => ['void', 'flags='=>'int'], -'uksort' => ['bool', '&rw_array_arg'=>'array', 'cmp_function'=>'callable(array-key,array-key):int'], +'uksort' => ['bool', '&rw_array_arg'=>'array', 'callback'=>'callable(array-key,array-key):int'], 'umask' => ['int', 'mask='=>'int'], 'UnderflowException::__clone' => ['void'], 'UnderflowException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Throwable)|(?UnderflowException)'], @@ -12624,7 +12619,7 @@ 'UnderflowException::getLine' => ['int'], 'UnderflowException::getMessage' => ['string'], 'UnderflowException::getPrevious' => ['Throwable|UnderflowException|null'], -'UnderflowException::getTrace' => ['array'], +'UnderflowException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'UnderflowException::getTraceAsString' => ['string'], 'UnexpectedValueException::__clone' => ['void'], 'UnexpectedValueException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Throwable)|(?UnexpectedValueException)'], @@ -12634,15 +12629,14 @@ 'UnexpectedValueException::getLine' => ['int'], 'UnexpectedValueException::getMessage' => ['string'], 'UnexpectedValueException::getPrevious' => ['Throwable|UnexpectedValueException|null'], -'UnexpectedValueException::getTrace' => ['array'], +'UnexpectedValueException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'UnexpectedValueException::getTraceAsString' => ['string'], -'uniqid' => ['string', 'prefix='=>'string', 'more_entropy='=>'bool'], +'uniqid' => ['non-empty-string', 'prefix='=>'string', 'more_entropy='=>'bool'], 'unixtojd' => ['int|false', 'timestamp='=>'int'], 'unlink' => ['bool', 'filename'=>'string', 'context='=>'resource'], 'unpack' => ['array|false', 'format'=>'string', 'data'=>'string', 'offset='=>'int'], 'unregister_tick_function' => ['void', 'function_name'=>'callable'], 'unserialize' => ['mixed', 'variable_representation'=>'string', 'allowed_classes='=>'array{allowed_classes?:string[]|bool}'], -'unset' => ['void', 'var='=>'mixed', '...args='=>'mixed'], 'untaint' => ['bool', '&rw_string'=>'string', '&...rw_strings='=>'string'], 'uopz_add_function' => ['bool', 'class'=>'string', 'function'=>'string', 'handler'=>'Closure', '$flags'=>'bool', '$all'=>'bool'], 'uopz_add_function\1' => ['bool', 'function'=>'string', 'handler'=>'Closure', '$flags'=>'bool'], @@ -12694,7 +12688,7 @@ 'urlencode' => ['string', 'str'=>'string'], 'use_soap_error_handler' => ['bool', 'handler='=>'bool'], 'usleep' => ['void', 'micro_seconds'=>'int'], -'usort' => ['bool', '&rw_array_arg'=>'array', 'cmp_function'=>'callable(mixed,mixed):int'], +'usort' => ['bool', '&rw_array_arg'=>'array', 'callback'=>'callable(mixed,mixed):int'], 'utf8_decode' => ['string', 'data'=>'string'], 'utf8_encode' => ['string', 'data'=>'string'], 'V8Js::__construct' => ['void', 'object_name='=>'string', 'variables='=>'array', 'extensions='=>'array', 'report_uncaught_exceptions='=>'bool', 'snapshot_blob='=>'string'], @@ -12730,7 +12724,7 @@ 'V8JsScriptException::getLine' => ['int'], 'V8JsScriptException::getMessage' => ['string'], 'V8JsScriptException::getPrevious' => ['Exception|Throwable'], -'V8JsScriptException::getTrace' => ['array'], +'V8JsScriptException::getTrace' => ['array{function:string,line?:int,file?:string,class?:class-string,type?:string,args?:mixed[],object?:object}'], 'V8JsScriptException::getTraceAsString' => ['string'], 'var_dump' => ['void', 'var'=>'mixed', '...args='=>'mixed'], 'var_export' => ['string|null', 'var'=>'mixed', 'return='=>'bool'], @@ -12973,6 +12967,7 @@ 'xdebug_call_line' => ['int', 'depth=' => 'int'], 'xdebug_clear_aggr_profiling_data' => ['bool'], 'xdebug_code_coverage_started' => ['bool'], +'xdebug_connect_to_client' => ['bool'], 'xdebug_debug_zval' => ['void', '...varName'=>'string'], 'xdebug_debug_zval_stdout' => ['void', '...varName'=>'string'], 'xdebug_disable' => ['void'], @@ -12993,6 +12988,7 @@ 'xdebug_is_debugger_active' => ['bool'], 'xdebug_is_enabled' => ['bool'], 'xdebug_memory_usage' => ['int'], +'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'], @@ -13215,7 +13211,7 @@ 'xslt_set_scheme_handler' => ['', 'xh'=>'', 'handlers'=>'array'], 'xslt_set_scheme_handlers' => ['', 'xh'=>'', 'handlers'=>'array'], 'xslt_setopt' => ['', 'processor'=>'', 'newmask'=>'int'], -'XSLTProcessor::getParameter' => ['string', 'namespaceuri'=>'string', 'localname'=>'string'], +'XSLTProcessor::getParameter' => ['string|false', 'namespaceuri'=>'string', 'localname'=>'string'], 'XsltProcessor::getSecurityPrefs' => ['int'], 'XSLTProcessor::hasExsltSupport' => ['bool'], 'XSLTProcessor::importStylesheet' => ['bool', 'stylesheet'=>'object'], @@ -13225,9 +13221,9 @@ 'XSLTProcessor::setParameter\'1' => ['bool', 'namespace'=>'string', 'options'=>'array'], 'XSLTProcessor::setProfiling' => ['bool', 'filename'=>'string'], 'XsltProcessor::setSecurityPrefs' => ['int', 'securityPrefs'=>'int'], -'XSLTProcessor::transformToDoc' => ['DOMDocument', 'doc'=>'DOMNode'], +'XSLTProcessor::transformToDoc' => ['DOMDocument|false', 'doc'=>'DOMNode'], 'XSLTProcessor::transformToURI' => ['int', 'doc'=>'DOMDocument', 'uri'=>'string'], -'XSLTProcessor::transformToXML' => ['string|false', 'doc'=>'DOMDocument|SimpleXMLElement'], +'XSLTProcessor::transformToXML' => ['string|false|null', 'doc'=>'DOMDocument|SimpleXMLElement'], 'Yaconf::get' => ['mixed', 'name'=>'string', 'default_value='=>'mixed'], 'Yaconf::has' => ['bool', 'name'=>'string'], 'Yaf_Action_Abstract::__construct' => ['void', 'request'=>'Yaf_Request_Abstract', 'response'=>'Yaf_Response_Abstract', 'view'=>'Yaf_View_Interface', 'invokeArgs='=>'?array'], diff --git a/resources/functionMap_php74delta.php b/resources/functionMap_php74delta.php index 656d998997..ff3f99ba0d 100644 --- a/resources/functionMap_php74delta.php +++ b/resources/functionMap_php74delta.php @@ -39,13 +39,13 @@ 'FFI::typeof' => ['FFI\CType', '&ptr'=>'FFI\CData'], 'FFI::type' => ['FFI\CType', 'type'=>'string'], 'get_mangled_object_vars' => ['array', 'obj'=>'object'], - 'mb_str_split' => ['array|false', 'str'=>'string', 'split_length='=>'int', 'encoding='=>'string'], + 'mb_str_split' => ['non-empty-array|false', 'str'=>'string', 'split_length='=>'int', 'encoding='=>'string'], 'password_algos' => ['array'], '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', 'subject'=>'string|array', 'limit='=>'int', '&w_count='=>'int', 'flags='=>'int'], + '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'], - 'sapi_windows_set_ctrl_handler' => ['bool', 'callable'=>'callable', 'add='=>'bool'], + 'sapi_windows_set_ctrl_handler' => ['bool', 'callable'=>'callable(int):void', 'add='=>'bool'], 'ReflectionProperty::getType' => ['?ReflectionType'], 'ReflectionProperty::hasType' => ['bool'], 'ReflectionProperty::isInitialized' => ['bool', 'object='=>'?object'], @@ -55,6 +55,7 @@ 'strip_tags' => ['string', 'str'=>'string', 'allowable_tags='=>'string|array'], 'WeakReference::create' => ['WeakReference', 'referent'=>'object'], 'WeakReference::get' => ['?object'], + 'proc_open' => ['resource|false', 'command'=>'string|list', 'descriptorspec'=>'array', '&w_pipes'=>'resource[]', 'cwd='=>'?string', 'env='=>'?array', 'other_options='=>'array'], ], 'old' => [ 'implode\'2' => ['string', 'pieces'=>'array', 'glue'=>'string'], diff --git a/resources/functionMap_php80delta.php b/resources/functionMap_php80delta.php index c42dbbcd3b..fb557075d8 100644 --- a/resources/functionMap_php80delta.php +++ b/resources/functionMap_php80delta.php @@ -25,6 +25,7 @@ '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'], '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'], @@ -38,13 +39,16 @@ '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' => ['array', 'separator'=>'string', 'str'=>'string', 'limit='=>'int'], + 'explode' => ['non-empty-array', 'separator'=>'non-empty-string', 'str'=>'string', 'limit='=>'int'], 'fdiv' => ['float', 'dividend'=>'float', 'divisor'=>'float'], '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_hkdf' => ['string', 'algo'=>'string', 'ikm'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], + '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_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'], 'imagecreate' => ['false|object', 'x_size'=>'int', 'y_size'=>'int'], 'imagecreatefrombmp' => ['false|object', 'filename'=>'string'], @@ -66,24 +70,29 @@ 'imagegrabscreen' => ['false|object'], 'imagegrabwindow' => ['false|object', 'window_handle'=>'int', 'client_area='=>'int'], 'imagejpeg' => ['bool', 'im'=>'GdImage', 'filename='=>'string|resource|null', 'quality='=>'int'], - 'imagejpeg\'1' => ['string|false', 'im'=>'GdImage', 'filename='=>'null', 'quality='=>'int'], 'imagerotate' => ['false|object', 'src_im'=>'resource', 'angle'=>'float', 'bgdcolor'=>'int', 'ignoretransparent='=>'int'], 'imagescale' => ['false|object', 'im'=>'resource', 'new_width'=>'int', 'new_height='=>'int', 'method='=>'int'], + 'ldap_set_rebind_proc' => ['bool', 'ldap'=>'resource', 'callback'=>'?callable'], 'mb_decode_numericentity' => ['string|false', 'string'=>'string', 'convmap'=>'array', 'encoding='=>'string'], - 'mb_str_split' => ['array', 'str'=>'string', 'split_length='=>'int', 'encoding='=>'string'], + 'mb_str_split' => ['non-empty-array', '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'], + '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[]'], 'PhpToken::isIgnorable' => ['bool'], 'PhpToken::getTokenName' => ['string'], + 'preg_match_all' => ['0|positive-int|false', 'pattern'=>'string', 'subject'=>'string', '&w_subpatterns='=>'array', 'flags='=>'int', 'offset='=>'int'], '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'], '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' => ['array', 'str'=>'string', 'split_length='=>'int'], + 'str_split' => ['non-empty-array', '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'], @@ -94,6 +103,7 @@ 'strripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], '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'], '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'], @@ -151,7 +161,6 @@ 'bcpowmod' => ['?string', 'base'=>'string', 'exponent'=>'string', 'modulus'=>'string', 'scale='=>'int'], 'com_load_typelib' => ['bool', 'typelib_name'=>'string', 'case_insensitive='=>'bool'], 'count_chars' => ['array|false|string', 'input'=>'string', 'mode='=>'int'], - 'create_function' => ['string', 'args'=>'string', 'code'=>'string'], 'date_add' => ['DateTime|false', 'object'=>'DateTime', 'interval'=>'DateInterval'], 'date_date_set' => ['DateTime|false', 'object'=>'DateTime', 'year'=>'int', 'month'=>'int', 'day'=>'int'], 'date_diff' => ['DateInterval|false', 'obj1'=>'DateTimeInterface', 'obj2'=>'DateTimeInterface', 'absolute='=>'bool'], @@ -168,7 +177,10 @@ 'gmmktime' => ['int|false', 'hour='=>'int', 'minute='=>'int', 'second='=>'int', 'month='=>'int', 'day='=>'int', 'year='=>'int'], 'gmp_random' => ['GMP', 'limiter='=>'int'], 'gzgetss' => ['string|false', 'zp'=>'resource', 'length'=>'int', 'allowable_tags='=>'string'], - 'hash_hkdf' => ['string|false', 'algo'=>'string', 'ikm'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], + 'hash' => ['non-empty-string|false', 'algo'=>'string', 'data'=>'string', '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_pbkdf2' => ['non-empty-string|false', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'raw_output='=>'bool'], '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'], @@ -191,20 +203,22 @@ 'imagegrabscreen' => ['false|resource'], 'imagegrabwindow' => ['false|resource', 'window_handle'=>'int', 'client_area='=>'int'], 'imagejpeg' => ['bool', 'im'=>'resource', 'filename='=>'string|resource|null', 'quality='=>'int'], - 'imagejpeg\'1' => ['string|false', 'im'=>'resource', 'filename='=>'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'], 'implode\'1' => ['string', 'pieces'=>'array'], 'jpeg2wbmp' => ['bool', 'jpegname'=>'string', 'wbmpname'=>'string', 'dest_height'=>'int', 'dest_width'=>'int', 'threshold'=>'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'], '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'], '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'], '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'], @@ -215,6 +229,7 @@ 'strripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], 'strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], 'strstr' => ['string|false', 'haystack'=>'string', 'needle'=>'string|int', 'before_needle='=>'bool'], + 'substr' => ['__benevolent', 'string'=>'string', 'start'=>'int', 'length='=>'int'], 'version_compare' => ['int|bool', 'version1'=>'string', 'version2'=>'string', 'operator='=>'string'], 'xml_parser_create' => ['resource', 'encoding='=>'string'], 'xml_parser_create_ns' => ['resource', 'encoding='=>'string', 'sep='=>'string'], diff --git a/resources/functionMetadata.php b/resources/functionMetadata.php index f136e361e4..fb6a45857c 100644 --- a/resources/functionMetadata.php +++ b/resources/functionMetadata.php @@ -68,6 +68,7 @@ 'DateTimeImmutable::setTimestamp' => ['hasSideEffects' => false], 'DateTimeImmutable::setTimezone' => ['hasSideEffects' => false], 'DateTimeImmutable::sub' => ['hasSideEffects' => false], + 'Error::__construct' => ['hasSideEffects' => false], 'ErrorException::__construct' => ['hasSideEffects' => false], 'Event::__construct' => ['hasSideEffects' => false], 'EventBase::getFeatures' => ['hasSideEffects' => false], @@ -86,6 +87,7 @@ 'EventHttpConnection::__construct' => ['hasSideEffects' => false], 'EventHttpRequest::__construct' => ['hasSideEffects' => false], 'EventHttpRequest::getCommand' => ['hasSideEffects' => false], + 'EventHttpRequest::getConnection' => ['hasSideEffects' => false], 'EventHttpRequest::getHost' => ['hasSideEffects' => false], 'EventHttpRequest::getInputBuffer' => ['hasSideEffects' => false], 'EventHttpRequest::getInputHeaders' => ['hasSideEffects' => false], @@ -495,6 +497,7 @@ 'ReflectionFunctionAbstract::getAttributes' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::getClosureScopeClass' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::getClosureThis' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::getClosureUsedVariables' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::getDocComment' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::getEndLine' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::getExtension' => ['hasSideEffects' => false], @@ -509,10 +512,13 @@ 'ReflectionFunctionAbstract::getShortName' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::getStartLine' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::getStaticVariables' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::getTentativeReturnType' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::hasTentativeReturnType' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::isClosure' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::isDeprecated' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::isGenerator' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::isInternal' => ['hasSideEffects' => false], + 'ReflectionFunctionAbstract::isStatic' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::isUserDefined' => ['hasSideEffects' => false], 'ReflectionFunctionAbstract::isVariadic' => ['hasSideEffects' => false], 'ReflectionGenerator::getExecutingFile' => ['hasSideEffects' => false], @@ -521,6 +527,7 @@ 'ReflectionGenerator::getFunction' => ['hasSideEffects' => false], 'ReflectionGenerator::getThis' => ['hasSideEffects' => false], 'ReflectionGenerator::getTrace' => ['hasSideEffects' => false], + 'ReflectionIntersectionType::getTypes' => ['hasSideEffects' => false], 'ReflectionMethod::getClosure' => ['hasSideEffects' => false], 'ReflectionMethod::getDeclaringClass' => ['hasSideEffects' => false], 'ReflectionMethod::getModifiers' => ['hasSideEffects' => false], @@ -533,6 +540,7 @@ 'ReflectionMethod::isProtected' => ['hasSideEffects' => false], 'ReflectionMethod::isPublic' => ['hasSideEffects' => false], 'ReflectionMethod::isStatic' => ['hasSideEffects' => false], + 'ReflectionMethod::setAccessible' => ['hasSideEffects' => false], 'ReflectionNamedType::getName' => ['hasSideEffects' => false], 'ReflectionNamedType::isBuiltin' => ['hasSideEffects' => false], 'ReflectionParameter::getAttributes' => ['hasSideEffects' => false], @@ -567,6 +575,7 @@ 'ReflectionProperty::isProtected' => ['hasSideEffects' => false], 'ReflectionProperty::isPublic' => ['hasSideEffects' => false], 'ReflectionProperty::isStatic' => ['hasSideEffects' => false], + 'ReflectionProperty::setAccessible' => ['hasSideEffects' => false], 'ReflectionReference::getId' => ['hasSideEffects' => false], 'ReflectionType::isBuiltin' => ['hasSideEffects' => false], 'ReflectionUnionType::getTypes' => ['hasSideEffects' => false], @@ -601,15 +610,11 @@ 'SimpleXMLIterator::valid' => ['hasSideEffects' => false], 'SoapFault::__construct' => ['hasSideEffects' => false], 'Spoofchecker::__construct' => ['hasSideEffects' => false], - 'StubTests\\Model\\BasePHPElement::getFQN' => ['hasSideEffects' => false], - 'StubTests\\Model\\BasePHPElement::getTypeNameFromNode' => ['hasSideEffects' => false], - 'StubTests\\Model\\BasePHPElement::hasMutedProblem' => ['hasSideEffects' => false], - 'StubTests\\Model\\StubsContainer::getClass' => ['hasSideEffects' => false], - 'StubTests\\Model\\StubsContainer::getInterface' => ['hasSideEffects' => false], + 'StubTests\\CodeStyle\\BracesOneLineFixer::getDefinition' => ['hasSideEffects' => false], 'StubTests\\Parsers\\ExpectedFunctionArgumentsInfo::__toString' => ['hasSideEffects' => false], 'StubTests\\Parsers\\Visitors\\CoreStubASTVisitor::__construct' => ['hasSideEffects' => false], + 'StubTests\\StubsMetaExpectedArgumentsTest::getClassMemberFqn' => ['hasSideEffects' => false], 'StubTests\\StubsParameterNamesTest::printParameters' => ['hasSideEffects' => false], - 'StubTests\\StubsTest::getParameterRepresentation' => ['hasSideEffects' => false], 'Transliterator::createInverse' => ['hasSideEffects' => false], 'Transliterator::getErrorCode' => ['hasSideEffects' => false], 'Transliterator::getErrorMessage' => ['hasSideEffects' => false], @@ -658,6 +663,7 @@ 'array_intersect_key' => ['hasSideEffects' => false], 'array_intersect_uassoc' => ['hasSideEffects' => false], 'array_intersect_ukey' => ['hasSideEffects' => false], + 'array_is_list' => ['hasSideEffects' => false], 'array_key_exists' => ['hasSideEffects' => false], 'array_key_first' => ['hasSideEffects' => false], 'array_key_last' => ['hasSideEffects' => false], @@ -711,7 +717,10 @@ 'ceil' => ['hasSideEffects' => false], 'checkdate' => ['hasSideEffects' => false], 'checkdnsrr' => ['hasSideEffects' => false], + 'chgrp' => ['hasSideEffects' => true], + 'chmod' => ['hasSideEffects' => true], 'chop' => ['hasSideEffects' => false], + 'chown' => ['hasSideEffects' => true], 'chr' => ['hasSideEffects' => false], 'chunk_split' => ['hasSideEffects' => false], 'class_implements' => ['hasSideEffects' => false], @@ -732,6 +741,7 @@ 'convert_cyr_string' => ['hasSideEffects' => false], 'convert_uudecode' => ['hasSideEffects' => false], 'convert_uuencode' => ['hasSideEffects' => false], + 'copy' => ['hasSideEffects' => true], 'cos' => ['hasSideEffects' => false], 'cosh' => ['hasSideEffects' => false], 'count' => ['hasSideEffects' => false], @@ -806,6 +816,7 @@ 'deg2rad' => ['hasSideEffects' => false], 'dirname' => ['hasSideEffects' => false], 'disk_free_space' => ['hasSideEffects' => false], + 'disk_total_space' => ['hasSideEffects' => false], 'diskfreespace' => ['hasSideEffects' => false], 'dngettext' => ['hasSideEffects' => false], 'doubleval' => ['hasSideEffects' => false], @@ -816,8 +827,18 @@ 'explode' => ['hasSideEffects' => false], 'expm1' => ['hasSideEffects' => false], 'extension_loaded' => ['hasSideEffects' => false], + 'fclose' => ['hasSideEffects' => true], 'fdiv' => ['hasSideEffects' => false], + 'feof' => ['hasSideEffects' => false], + 'fflush' => ['hasSideEffects' => true], + 'fgetc' => ['hasSideEffects' => true], + 'fgetcsv' => ['hasSideEffects' => true], + 'fgets' => ['hasSideEffects' => true], + 'fgetss' => ['hasSideEffects' => true], + 'file' => ['hasSideEffects' => false], 'file_exists' => ['hasSideEffects' => false], + 'file_get_contents' => ['hasSideEffects' => false], + 'file_put_contents' => ['hasSideEffects' => true], 'fileatime' => ['hasSideEffects' => false], 'filectime' => ['hasSideEffects' => false], 'filegroup' => ['hasSideEffects' => false], @@ -837,13 +858,26 @@ 'finfo::buffer' => ['hasSideEffects' => false], 'finfo::file' => ['hasSideEffects' => false], 'floatval' => ['hasSideEffects' => false], + 'flock' => ['hasSideEffects' => true], 'floor' => ['hasSideEffects' => false], 'fmod' => ['hasSideEffects' => false], + 'fnmatch' => ['hasSideEffects' => false], + 'fopen' => ['hasSideEffects' => true], + 'fpassthru' => ['hasSideEffects' => true], + 'fputcsv' => ['hasSideEffects' => true], + 'fputs' => ['hasSideEffects' => true], + 'fread' => ['hasSideEffects' => true], + 'fscanf' => ['hasSideEffects' => true], + 'fseek' => ['hasSideEffects' => true], + 'fstat' => ['hasSideEffects' => false], + 'ftell' => ['hasSideEffects' => false], 'ftok' => ['hasSideEffects' => false], + 'ftruncate' => ['hasSideEffects' => true], 'func_get_arg' => ['hasSideEffects' => false], 'func_get_args' => ['hasSideEffects' => false], 'func_num_args' => ['hasSideEffects' => false], 'function_exists' => ['hasSideEffects' => false], + 'fwrite' => ['hasSideEffects' => true], 'gc_enabled' => ['hasSideEffects' => false], 'gc_status' => ['hasSideEffects' => false], 'gd_info' => ['hasSideEffects' => false], @@ -948,9 +982,6 @@ 'gmp_pow' => ['hasSideEffects' => false], 'gmp_powm' => ['hasSideEffects' => false], 'gmp_prob_prime' => ['hasSideEffects' => false], - 'gmp_random' => ['hasSideEffects' => false], - 'gmp_random_bits' => ['hasSideEffects' => false], - 'gmp_random_range' => ['hasSideEffects' => false], 'gmp_root' => ['hasSideEffects' => false], 'gmp_rootrem' => ['hasSideEffects' => false], 'gmp_scan0' => ['hasSideEffects' => false], @@ -1172,8 +1203,11 @@ 'key' => ['hasSideEffects' => false], 'key_exists' => ['hasSideEffects' => false], 'lcfirst' => ['hasSideEffects' => false], + 'lchgrp' => ['hasSideEffects' => true], + 'lchown' => ['hasSideEffects' => true], 'libxml_get_errors' => ['hasSideEffects' => false], 'libxml_get_last_error' => ['hasSideEffects' => false], + 'link' => ['hasSideEffects' => true], 'linkinfo' => ['hasSideEffects' => false], 'locale_accept_from_http' => ['hasSideEffects' => false], 'locale_canonicalize' => ['hasSideEffects' => false], @@ -1260,7 +1294,9 @@ 'mhash_keygen_s2k' => ['hasSideEffects' => false], 'microtime' => ['hasSideEffects' => false], 'min' => ['hasSideEffects' => false], + 'mkdir' => ['hasSideEffects' => true], 'mktime' => ['hasSideEffects' => false], + 'move_uploaded_file' => ['hasSideEffects' => true], 'msgfmt_create' => ['hasSideEffects' => false], 'msgfmt_format' => ['hasSideEffects' => false], 'msgfmt_format_message' => ['hasSideEffects' => false], @@ -1292,13 +1328,16 @@ 'numfmt_get_text_attribute' => ['hasSideEffects' => false], 'numfmt_parse' => ['hasSideEffects' => false], 'ob_etaghandler' => ['hasSideEffects' => false], + 'ob_get_contents' => ['hasSideEffects' => false], 'ob_iconv_handler' => ['hasSideEffects' => false], 'octdec' => ['hasSideEffects' => false], 'ord' => ['hasSideEffects' => false], 'pack' => ['hasSideEffects' => false], + 'parse_ini_file' => ['hasSideEffects' => false], 'parse_ini_string' => ['hasSideEffects' => false], 'parse_url' => ['hasSideEffects' => false], 'pathinfo' => ['hasSideEffects' => false], + 'pclose' => ['hasSideEffects' => true], 'pcntl_errno' => ['hasSideEffects' => false], 'pcntl_get_last_error' => ['hasSideEffects' => false], 'pcntl_getpriority' => ['hasSideEffects' => false], @@ -1319,6 +1358,7 @@ 'php_uname' => ['hasSideEffects' => false], 'phpversion' => ['hasSideEffects' => false], 'pi' => ['hasSideEffects' => false], + 'popen' => ['hasSideEffects' => true], 'pos' => ['hasSideEffects' => false], 'posix_ctermid' => ['hasSideEffects' => false], 'posix_errno' => ['hasSideEffects' => false], @@ -1363,15 +1403,20 @@ 'range' => ['hasSideEffects' => false], 'rawurldecode' => ['hasSideEffects' => false], 'rawurlencode' => ['hasSideEffects' => false], + 'readfile' => ['hasSideEffects' => true], + 'readlink' => ['hasSideEffects' => false], 'realpath' => ['hasSideEffects' => false], 'realpath_cache_get' => ['hasSideEffects' => false], 'realpath_cache_size' => ['hasSideEffects' => false], + 'rename' => ['hasSideEffects' => true], 'resourcebundle_count' => ['hasSideEffects' => false], 'resourcebundle_create' => ['hasSideEffects' => false], 'resourcebundle_get' => ['hasSideEffects' => false], 'resourcebundle_get_error_code' => ['hasSideEffects' => false], 'resourcebundle_get_error_message' => ['hasSideEffects' => false], 'resourcebundle_locales' => ['hasSideEffects' => false], + 'rewind' => ['hasSideEffects' => true], + 'rmdir' => ['hasSideEffects' => true], 'round' => ['hasSideEffects' => false], 'rtrim' => ['hasSideEffects' => false], 'sha1' => ['hasSideEffects' => false], @@ -1391,7 +1436,6 @@ 'str_pad' => ['hasSideEffects' => false], 'str_repeat' => ['hasSideEffects' => false], 'str_rot13' => ['hasSideEffects' => false], - 'str_shuffle' => ['hasSideEffects' => false], 'str_split' => ['hasSideEffects' => false], 'str_starts_with' => ['hasSideEffects' => false], 'str_word_count' => ['hasSideEffects' => false], @@ -1433,9 +1477,11 @@ 'substr_compare' => ['hasSideEffects' => false], 'substr_count' => ['hasSideEffects' => false], 'substr_replace' => ['hasSideEffects' => false], + 'symlink' => ['hasSideEffects' => true], 'sys_getloadavg' => ['hasSideEffects' => false], 'tan' => ['hasSideEffects' => false], 'tanh' => ['hasSideEffects' => false], + 'tempnam' => ['hasSideEffects' => true], 'timezone_abbreviations_list' => ['hasSideEffects' => false], 'timezone_identifiers_list' => ['hasSideEffects' => false], 'timezone_location_get' => ['hasSideEffects' => false], @@ -1445,8 +1491,10 @@ 'timezone_open' => ['hasSideEffects' => false], 'timezone_transitions_get' => ['hasSideEffects' => false], 'timezone_version_get' => ['hasSideEffects' => false], + 'tmpfile' => ['hasSideEffects' => true], 'token_get_all' => ['hasSideEffects' => false], 'token_name' => ['hasSideEffects' => false], + 'touch' => ['hasSideEffects' => true], 'transliterator_create' => ['hasSideEffects' => false], 'transliterator_create_from_rules' => ['hasSideEffects' => false], 'transliterator_create_inverse' => ['hasSideEffects' => false], @@ -1457,7 +1505,8 @@ 'trim' => ['hasSideEffects' => false], 'ucfirst' => ['hasSideEffects' => false], 'ucwords' => ['hasSideEffects' => false], - 'uniqid' => ['hasSideEffects' => false], + 'umask' => ['hasSideEffects' => true], + 'unlink' => ['hasSideEffects' => true], 'unpack' => ['hasSideEffects' => false], 'urldecode' => ['hasSideEffects' => false], 'urlencode' => ['hasSideEffects' => false], diff --git a/src/AnalysedCodeException.php b/src/AnalysedCodeException.php index d4e7558622..6d78350e50 100644 --- a/src/AnalysedCodeException.php +++ b/src/AnalysedCodeException.php @@ -2,7 +2,9 @@ namespace PHPStan; -abstract class AnalysedCodeException extends \Exception +use Exception; + +abstract class AnalysedCodeException extends Exception { abstract public function getTip(): ?string; diff --git a/src/Analyser/Analyser.php b/src/Analyser/Analyser.php index 6941c4a0d6..0ddf4b727c 100644 --- a/src/Analyser/Analyser.php +++ b/src/Analyser/Analyser.php @@ -2,49 +2,38 @@ namespace PHPStan\Analyser; +use Closure; use PHPStan\Rules\Registry; +use Throwable; +use function array_fill_keys; +use function array_merge; +use function count; +use function sprintf; class Analyser { - private \PHPStan\Analyser\FileAnalyser $fileAnalyser; - - private Registry $registry; - - private \PHPStan\Analyser\NodeScopeResolver $nodeScopeResolver; - - private int $internalErrorsCountLimit; - - /** @var \PHPStan\Analyser\Error[] */ - private array $collectedErrors = []; - public function __construct( - FileAnalyser $fileAnalyser, - Registry $registry, - NodeScopeResolver $nodeScopeResolver, - int $internalErrorsCountLimit + private FileAnalyser $fileAnalyser, + private Registry $registry, + private NodeScopeResolver $nodeScopeResolver, + private int $internalErrorsCountLimit, ) { - $this->fileAnalyser = $fileAnalyser; - $this->registry = $registry; - $this->nodeScopeResolver = $nodeScopeResolver; - $this->internalErrorsCountLimit = $internalErrorsCountLimit; } /** * @param string[] $files - * @param \Closure(string $file): void|null $preFileCallback - * @param \Closure(int): void|null $postFileCallback - * @param bool $debug + * @param Closure(string $file): void|null $preFileCallback + * @param Closure(int ): void|null $postFileCallback * @param string[]|null $allAnalysedFiles - * @return AnalyserResult */ public function analyse( array $files, - ?\Closure $preFileCallback = null, - ?\Closure $postFileCallback = null, + ?Closure $preFileCallback = null, + ?Closure $postFileCallback = null, bool $debug = false, - ?array $allAnalysedFiles = null + ?array $allAnalysedFiles = null, ): AnalyserResult { if ($allAnalysedFiles === null) { @@ -54,8 +43,6 @@ public function analyse( $this->nodeScopeResolver->setAnalysedFiles($allAnalysedFiles); $allAnalysedFiles = array_fill_keys($allAnalysedFiles, true); - $this->collectErrors($files); - $errors = []; $internalErrorsCount = 0; $reachedInternalErrorsCountLimit = false; @@ -71,7 +58,7 @@ public function analyse( $file, $allAnalysedFiles, $this->registry, - null + null, ); $errors = array_merge($errors, $fileAnalyserResult->getErrors()); $dependencies[$file] = $fileAnalyserResult->getDependencies(); @@ -80,7 +67,7 @@ public function analyse( if (count($fileExportedNodes) > 0) { $exportedNodes[$file] = $fileExportedNodes; } - } catch (\Throwable $t) { + } catch (Throwable $t) { if ($debug) { throw $t; } @@ -90,7 +77,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.md', ); $errors[] = new Error($internalErrorMessage, $file, null, $t); if ($internalErrorsCount >= $this->internalErrorsCountLimit) { @@ -106,44 +93,13 @@ public function analyse( $postFileCallback(1); } - $this->restoreCollectErrorsHandler(); - - $errors = array_merge($errors, $this->collectedErrors); - return new AnalyserResult( $errors, [], $internalErrorsCount === 0 ? $dependencies : null, $exportedNodes, - $reachedInternalErrorsCountLimit + $reachedInternalErrorsCountLimit, ); } - /** - * @param string[] $analysedFiles - */ - private function collectErrors(array $analysedFiles): void - { - $this->collectedErrors = []; - set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) use ($analysedFiles): bool { - if (error_reporting() === 0) { - // silence @ operator - return true; - } - - if (!in_array($errfile, $analysedFiles, true)) { - return true; - } - - $this->collectedErrors[] = new Error($errstr, $errfile, $errline, true); - - return true; - }); - } - - private function restoreCollectErrorsHandler(): void - { - restore_error_handler(); - } - } diff --git a/src/Analyser/AnalyserResult.php b/src/Analyser/AnalyserResult.php index d045f84749..3966d008b3 100644 --- a/src/Analyser/AnalyserResult.php +++ b/src/Analyser/AnalyserResult.php @@ -3,68 +3,46 @@ namespace PHPStan\Analyser; use PHPStan\Dependency\ExportedNode; +use function usort; class AnalyserResult { - /** @var \PHPStan\Analyser\Error[] */ + /** @var Error[] */ private array $unorderedErrors; - /** @var \PHPStan\Analyser\Error[] */ - private array $errors; - - /** @var string[] */ - private array $internalErrors; - - /** @var array>|null */ - private ?array $dependencies; - - /** @var array> */ - private array $exportedNodes; - - private bool $reachedInternalErrorsCountLimit; - /** - * @param \PHPStan\Analyser\Error[] $errors + * @param Error[] $errors * @param string[] $internalErrors * @param array>|null $dependencies * @param array> $exportedNodes - * @param bool $reachedInternalErrorsCountLimit */ public function __construct( - array $errors, - array $internalErrors, - ?array $dependencies, - array $exportedNodes, - bool $reachedInternalErrorsCountLimit + private array $errors, + private array $internalErrors, + private ?array $dependencies, + private array $exportedNodes, + private bool $reachedInternalErrorsCountLimit, ) { $this->unorderedErrors = $errors; usort( - $errors, - static function (Error $a, Error $b): int { - return [ - $a->getFile(), - $a->getLine(), - $a->getMessage(), - ] <=> [ - $b->getFile(), - $b->getLine(), - $b->getMessage(), - ]; - } + $this->errors, + static fn (Error $a, Error $b): int => [ + $a->getFile(), + $a->getLine(), + $a->getMessage(), + ] <=> [ + $b->getFile(), + $b->getLine(), + $b->getMessage(), + ], ); - - $this->errors = $errors; - $this->internalErrors = $internalErrors; - $this->dependencies = $dependencies; - $this->exportedNodes = $exportedNodes; - $this->reachedInternalErrorsCountLimit = $reachedInternalErrorsCountLimit; } /** - * @return \PHPStan\Analyser\Error[] + * @return Error[] */ public function getUnorderedErrors(): array { @@ -72,7 +50,7 @@ public function getUnorderedErrors(): array } /** - * @return \PHPStan\Analyser\Error[] + * @return Error[] */ public function getErrors(): array { diff --git a/src/Analyser/ConditionalExpressionHolder.php b/src/Analyser/ConditionalExpressionHolder.php index 834dd49d03..b36e35bc26 100644 --- a/src/Analyser/ConditionalExpressionHolder.php +++ b/src/Analyser/ConditionalExpressionHolder.php @@ -2,31 +2,27 @@ namespace PHPStan\Analyser; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function count; +use function implode; +use function sprintf; class ConditionalExpressionHolder { - /** @var array */ - private array $conditionExpressionTypes; - - private VariableTypeHolder $typeHolder; - /** * @param array $conditionExpressionTypes - * @param VariableTypeHolder $typeHolder */ public function __construct( - array $conditionExpressionTypes, - VariableTypeHolder $typeHolder + private array $conditionExpressionTypes, + private VariableTypeHolder $typeHolder, ) { if (count($conditionExpressionTypes) === 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - $this->conditionExpressionTypes = $conditionExpressionTypes; - $this->typeHolder = $typeHolder; } /** @@ -53,7 +49,7 @@ public function getKey(): string '%s => %s (%s)', implode(' && ', $parts), $this->typeHolder->getType()->describe(VerbosityLevel::precise()), - $this->typeHolder->getCertainty()->describe() + $this->typeHolder->getCertainty()->describe(), ); } diff --git a/src/Analyser/DirectScopeFactory.php b/src/Analyser/DirectScopeFactory.php index f8e7e43553..2299cd7dd2 100644 --- a/src/Analyser/DirectScopeFactory.php +++ b/src/Analyser/DirectScopeFactory.php @@ -2,13 +2,20 @@ namespace PHPStan\Analyser; +use PhpParser\PrettyPrinter\Standard; use PHPStan\DependencyInjection\Container; use PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider; use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider; +use PHPStan\Parser\Parser; +use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Type; +use function is_a; /** * @internal @@ -16,85 +23,43 @@ class DirectScopeFactory implements ScopeFactory { - private string $scopeClass; - - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider $dynamicReturnTypeExtensionRegistryProvider; - - private OperatorTypeSpecifyingExtensionRegistryProvider $operatorTypeSpecifyingExtensionRegistryProvider; - - private \PhpParser\PrettyPrinter\Standard $printer; - - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; - - private \PHPStan\Rules\Properties\PropertyReflectionFinder $propertyReflectionFinder; - - private \PHPStan\Parser\Parser $parser; - - private NodeScopeResolver $nodeScopeResolver; - - private bool $treatPhpDocTypesAsCertain; - - private bool $objectFromNewClass; - /** @var string[] */ private array $dynamicConstantNames; public function __construct( - string $scopeClass, - ReflectionProvider $reflectionProvider, - DynamicReturnTypeExtensionRegistryProvider $dynamicReturnTypeExtensionRegistryProvider, - OperatorTypeSpecifyingExtensionRegistryProvider $operatorTypeSpecifyingExtensionRegistryProvider, - \PhpParser\PrettyPrinter\Standard $printer, - TypeSpecifier $typeSpecifier, - PropertyReflectionFinder $propertyReflectionFinder, - \PHPStan\Parser\Parser $parser, - NodeScopeResolver $nodeScopeResolver, - bool $treatPhpDocTypesAsCertain, - bool $objectFromNewClass, - Container $container + private string $scopeClass, + private ReflectionProvider $reflectionProvider, + private DynamicReturnTypeExtensionRegistryProvider $dynamicReturnTypeExtensionRegistryProvider, + private OperatorTypeSpecifyingExtensionRegistryProvider $operatorTypeSpecifyingExtensionRegistryProvider, + private Standard $printer, + private TypeSpecifier $typeSpecifier, + private PropertyReflectionFinder $propertyReflectionFinder, + private Parser $parser, + private NodeScopeResolver $nodeScopeResolver, + private bool $treatPhpDocTypesAsCertain, + Container $container, + private PhpVersion $phpVersion, + private bool $explicitMixedInUnknownGenericNew, ) { - $this->scopeClass = $scopeClass; - $this->reflectionProvider = $reflectionProvider; - $this->dynamicReturnTypeExtensionRegistryProvider = $dynamicReturnTypeExtensionRegistryProvider; - $this->operatorTypeSpecifyingExtensionRegistryProvider = $operatorTypeSpecifyingExtensionRegistryProvider; - $this->printer = $printer; - $this->typeSpecifier = $typeSpecifier; - $this->propertyReflectionFinder = $propertyReflectionFinder; - $this->parser = $parser; - $this->nodeScopeResolver = $nodeScopeResolver; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; - $this->objectFromNewClass = $objectFromNewClass; $this->dynamicConstantNames = $container->getParameter('dynamicConstantNames'); } /** - * @param \PHPStan\Analyser\ScopeContext $context - * @param bool $declareStrictTypes - * @param array $constantTypes - * @param \PHPStan\Reflection\FunctionReflection|\PHPStan\Reflection\MethodReflection|null $function - * @param string|null $namespace - * @param \PHPStan\Analyser\VariableTypeHolder[] $variablesTypes - * @param \PHPStan\Analyser\VariableTypeHolder[] $moreSpecificTypes + * @param array $constantTypes + * @param VariableTypeHolder[] $variablesTypes + * @param VariableTypeHolder[] $moreSpecificTypes * @param array $conditionalExpressions - * @param string|null $inClosureBindScopeClass - * @param \PHPStan\Reflection\ParametersAcceptor|null $anonymousFunctionReflection - * @param bool $inFirstLevelStatement * @param array $currentlyAssignedExpressions * @param array $nativeExpressionTypes - * @param array<\PHPStan\Reflection\FunctionReflection|\PHPStan\Reflection\MethodReflection> $inFunctionCallsStack - * @param bool $afterExtractCall - * @param Scope|null $parentScope + * @param array<(FunctionReflection|MethodReflection)> $inFunctionCallsStack * - * @return MutatingScope */ public function create( ScopeContext $context, bool $declareStrictTypes = false, array $constantTypes = [], - $function = null, + FunctionReflection|MethodReflection|null $function = null, ?string $namespace = null, array $variablesTypes = [], array $moreSpecificTypes = [], @@ -106,12 +71,12 @@ public function create( array $nativeExpressionTypes = [], array $inFunctionCallsStack = [], bool $afterExtractCall = false, - ?Scope $parentScope = null + ?Scope $parentScope = null, ): MutatingScope { $scopeClass = $this->scopeClass; if (!is_a($scopeClass, MutatingScope::class, true)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return new $scopeClass( @@ -125,6 +90,7 @@ public function create( $this->parser, $this->nodeScopeResolver, $context, + $this->phpVersion, $declareStrictTypes, $constantTypes, $function, @@ -140,9 +106,9 @@ public function create( $inFunctionCallsStack, $this->dynamicConstantNames, $this->treatPhpDocTypesAsCertain, - $this->objectFromNewClass, $afterExtractCall, - $parentScope + $parentScope, + $this->explicitMixedInUnknownGenericNew, ); } diff --git a/src/Analyser/EnsuredNonNullabilityResult.php b/src/Analyser/EnsuredNonNullabilityResult.php index a949f46976..258a16b18b 100644 --- a/src/Analyser/EnsuredNonNullabilityResult.php +++ b/src/Analyser/EnsuredNonNullabilityResult.php @@ -5,19 +5,11 @@ class EnsuredNonNullabilityResult { - private MutatingScope $scope; - - /** @var EnsuredNonNullabilityResultExpression[] */ - private array $specifiedExpressions; - /** - * @param MutatingScope $scope * @param EnsuredNonNullabilityResultExpression[] $specifiedExpressions */ - public function __construct(MutatingScope $scope, array $specifiedExpressions) + public function __construct(private MutatingScope $scope, private array $specifiedExpressions) { - $this->scope = $scope; - $this->specifiedExpressions = $specifiedExpressions; } public function getScope(): MutatingScope diff --git a/src/Analyser/EnsuredNonNullabilityResultExpression.php b/src/Analyser/EnsuredNonNullabilityResultExpression.php index adc3ebfb22..a7ed1572f8 100644 --- a/src/Analyser/EnsuredNonNullabilityResultExpression.php +++ b/src/Analyser/EnsuredNonNullabilityResultExpression.php @@ -8,21 +8,12 @@ class EnsuredNonNullabilityResultExpression { - private Expr $expression; - - private Type $originalType; - - private Type $originalNativeType; - public function __construct( - Expr $expression, - Type $originalType, - Type $originalNativeType + private Expr $expression, + private Type $originalType, + private Type $originalNativeType, ) { - $this->expression = $expression; - $this->originalType = $originalType; - $this->originalNativeType = $originalNativeType; } public function getExpression(): Expr diff --git a/src/Analyser/Error.php b/src/Analyser/Error.php index 7a80edbf8b..f36ba89284 100644 --- a/src/Analyser/Error.php +++ b/src/Analyser/Error.php @@ -2,74 +2,38 @@ namespace PHPStan\Analyser; -class Error implements \JsonSerializable +use Exception; +use JsonSerializable; +use PhpParser\Node; +use PHPStan\ShouldNotHappenException; +use ReturnTypeWillChange; +use Throwable; +use function is_bool; + +/** @api */ +class Error implements JsonSerializable { - private string $message; - - private string $file; - - private ?int $line; - - /** @var bool|\Throwable */ - private $canBeIgnored; - - private ?string $filePath; - - private ?string $traitFilePath; - - private ?string $tip; - - private ?int $nodeLine; - - /** @phpstan-var class-string<\PhpParser\Node>|null */ - private ?string $nodeType; - - private ?string $identifier; - - /** @var mixed[] */ - private array $metadata; - /** * Error constructor. * - * @param string $message - * @param string $file - * @param int|null $line - * @param bool|\Throwable $canBeIgnored - * @param string|null $filePath - * @param string|null $traitFilePath - * @param string|null $tip - * @param int|null $nodeLine - * @param class-string<\PhpParser\Node>|null $nodeType - * @param string|null $identifier + * @param class-string|null $nodeType * @param mixed[] $metadata */ public function __construct( - string $message, - string $file, - ?int $line = null, - $canBeIgnored = true, - ?string $filePath = null, - ?string $traitFilePath = null, - ?string $tip = null, - ?int $nodeLine = null, - ?string $nodeType = null, - ?string $identifier = null, - array $metadata = [] + private string $message, + private string $file, + private ?int $line = null, + private bool|Throwable $canBeIgnored = true, + private ?string $filePath = null, + private ?string $traitFilePath = null, + private ?string $tip = null, + private ?int $nodeLine = null, + private ?string $nodeType = null, + private ?string $identifier = null, + private array $metadata = [], ) { - $this->message = $message; - $this->file = $file; - $this->line = $line; - $this->canBeIgnored = $canBeIgnored; - $this->filePath = $filePath; - $this->traitFilePath = $traitFilePath; - $this->tip = $tip; - $this->nodeLine = $nodeLine; - $this->nodeType = $nodeType; - $this->identifier = $identifier; - $this->metadata = $metadata; } public function getMessage(): string @@ -94,7 +58,7 @@ public function getFilePath(): string public function changeFilePath(string $newFilePath): self { if ($this->traitFilePath !== null) { - throw new \PHPStan\ShouldNotHappenException('Errors in traits not yet supported'); + throw new ShouldNotHappenException('Errors in traits not yet supported'); } return new self( @@ -108,7 +72,7 @@ public function changeFilePath(string $newFilePath): self $this->nodeLine, $this->nodeType, $this->identifier, - $this->metadata + $this->metadata, ); } @@ -125,7 +89,7 @@ public function changeTraitFilePath(string $newFilePath): self $this->nodeLine, $this->nodeType, $this->identifier, - $this->metadata + $this->metadata, ); } @@ -146,7 +110,7 @@ public function canBeIgnored(): bool public function hasNonIgnorableException(): bool { - return $this->canBeIgnored instanceof \Throwable; + return $this->canBeIgnored instanceof Throwable; } public function getTip(): ?string @@ -169,7 +133,26 @@ public function withoutTip(): self $this->traitFilePath, null, $this->nodeLine, - $this->nodeType + $this->nodeType, + ); + } + + public function doNotIgnore(): self + { + if (!$this->canBeIgnored()) { + return $this; + } + + return new self( + $this->message, + $this->file, + $this->line, + false, + $this->filePath, + $this->traitFilePath, + $this->tip, + $this->nodeLine, + $this->nodeType, ); } @@ -179,7 +162,7 @@ public function getNodeLine(): ?int } /** - * @return class-string<\PhpParser\Node>|null + * @return class-string|null */ public function getNodeType(): ?string { @@ -202,6 +185,7 @@ public function getMetadata(): array /** * @return mixed */ + #[ReturnTypeWillChange] public function jsonSerialize() { return [ @@ -221,7 +205,6 @@ public function jsonSerialize() /** * @param mixed[] $json - * @return self */ public static function decode(array $json): self { @@ -229,20 +212,19 @@ public static function decode(array $json): self $json['message'], $json['file'], $json['line'], - $json['canBeIgnored'] === 'exception' ? new \Exception() : $json['canBeIgnored'], + $json['canBeIgnored'] === 'exception' ? new Exception() : $json['canBeIgnored'], $json['filePath'], $json['traitFilePath'], $json['tip'], $json['nodeLine'] ?? null, $json['nodeType'] ?? null, $json['identifier'] ?? null, - $json['metadata'] ?? [] + $json['metadata'] ?? [], ); } /** * @param mixed[] $properties - * @return self */ public static function __set_state(array $properties): self { @@ -257,7 +239,7 @@ public static function __set_state(array $properties): self $properties['nodeLine'] ?? null, $properties['nodeType'] ?? null, $properties['identifier'] ?? null, - $properties['metadata'] ?? [] + $properties['metadata'] ?? [], ); } diff --git a/src/Analyser/ExpressionContext.php b/src/Analyser/ExpressionContext.php index 373ea50d0a..df1f55d284 100644 --- a/src/Analyser/ExpressionContext.php +++ b/src/Analyser/ExpressionContext.php @@ -7,21 +7,12 @@ class ExpressionContext { - private bool $isDeep; - - private ?string $inAssignRightSideVariableName; - - private ?Type $inAssignRightSideType; - private function __construct( - bool $isDeep, - ?string $inAssignRightSideVariableName, - ?Type $inAssignRightSideType + private bool $isDeep, + private ?string $inAssignRightSideVariableName, + private ?Type $inAssignRightSideType, ) { - $this->isDeep = $isDeep; - $this->inAssignRightSideVariableName = $inAssignRightSideVariableName; - $this->inAssignRightSideType = $inAssignRightSideType; } public static function createTopLevel(): self diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index 4cec309c24..93a3bdca25 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -5,13 +5,6 @@ class ExpressionResult { - private MutatingScope $scope; - - private bool $hasYield; - - /** @var ThrowPoint[] $throwPoints */ - private array $throwPoints; - /** @var (callable(): MutatingScope)|null */ private $truthyScopeCallback; @@ -23,23 +16,18 @@ class ExpressionResult private ?MutatingScope $falseyScope = null; /** - * @param MutatingScope $scope - * @param bool $hasYield * @param ThrowPoint[] $throwPoints * @param (callable(): MutatingScope)|null $truthyScopeCallback * @param (callable(): MutatingScope)|null $falseyScopeCallback */ public function __construct( - MutatingScope $scope, - bool $hasYield, - array $throwPoints, + private MutatingScope $scope, + private bool $hasYield, + private array $throwPoints, ?callable $truthyScopeCallback = null, - ?callable $falseyScopeCallback = null + ?callable $falseyScopeCallback = null, ) { - $this->scope = $scope; - $this->hasYield = $hasYield; - $this->throwPoints = $throwPoints; $this->truthyScopeCallback = $truthyScopeCallback; $this->falseyScopeCallback = $falseyScopeCallback; } diff --git a/src/Analyser/FileAnalyser.php b/src/Analyser/FileAnalyser.php index af21ba3f71..4d9cdb9297 100644 --- a/src/Analyser/FileAnalyser.php +++ b/src/Analyser/FileAnalyser.php @@ -4,6 +4,7 @@ use PhpParser\Comment; use PhpParser\Node; +use PHPStan\AnalysedCodeException; use PHPStan\BetterReflection\NodeCompiler\Exception\UnableToCompileNode; use PHPStan\BetterReflection\Reflection\Exception\NotAClassReflection; use PHPStan\BetterReflection\Reflection\Exception\NotAnInterfaceReflection; @@ -11,6 +12,7 @@ use PHPStan\Dependency\DependencyResolver; use PHPStan\Node\FileNode; use PHPStan\Parser\Parser; +use PHPStan\Parser\ParserErrorsException; use PHPStan\Rules\FileRuleError; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\LineRuleError; @@ -19,48 +21,46 @@ use PHPStan\Rules\Registry; use PHPStan\Rules\TipRuleError; use function array_key_exists; +use function array_keys; +use function array_merge; use function array_unique; +use function array_values; +use function error_reporting; +use function get_class; +use function is_dir; +use function is_file; +use function is_string; +use function restore_error_handler; +use function set_error_handler; +use function sprintf; +use function strpos; +use const E_DEPRECATED; class FileAnalyser { - private \PHPStan\Analyser\ScopeFactory $scopeFactory; - - private \PHPStan\Analyser\NodeScopeResolver $nodeScopeResolver; - - private \PHPStan\Parser\Parser $parser; - - private DependencyResolver $dependencyResolver; - - private bool $reportUnmatchedIgnoredErrors; + /** @var Error[] */ + private array $collectedErrors = []; public function __construct( - ScopeFactory $scopeFactory, - NodeScopeResolver $nodeScopeResolver, - Parser $parser, - DependencyResolver $dependencyResolver, - bool $reportUnmatchedIgnoredErrors + private ScopeFactory $scopeFactory, + private NodeScopeResolver $nodeScopeResolver, + private Parser $parser, + private DependencyResolver $dependencyResolver, + private bool $reportUnmatchedIgnoredErrors, ) { - $this->scopeFactory = $scopeFactory; - $this->nodeScopeResolver = $nodeScopeResolver; - $this->parser = $parser; - $this->dependencyResolver = $dependencyResolver; - $this->reportUnmatchedIgnoredErrors = $reportUnmatchedIgnoredErrors; } /** - * @param string $file * @param array $analysedFiles - * @param Registry $registry - * @param callable(\PhpParser\Node $node, Scope $scope): void|null $outerNodeCallback - * @return FileAnalyserResult + * @param callable(Node $node, Scope $scope): void|null $outerNodeCallback */ public function analyseFile( string $file, array $analysedFiles, Registry $registry, - ?callable $outerNodeCallback + ?callable $outerNodeCallback, ): FileAnalyserResult { $fileErrors = []; @@ -68,10 +68,20 @@ public function analyseFile( $exportedNodes = []; if (is_file($file)) { try { + $this->collectErrors($analysedFiles); $parserNodes = $this->parser->parseFile($file); - $linesToIgnore = []; + $linesToIgnore = $this->getLinesToIgnoreFromTokens($file, $parserNodes); $temporaryFileErrors = []; - $nodeCallback = function (\PhpParser\Node $node, Scope $scope) use (&$fileErrors, &$fileDependencies, &$exportedNodes, $file, $registry, $outerNodeCallback, $analysedFiles, &$linesToIgnore, &$temporaryFileErrors): void { + $nodeCallback = function (Node $node, Scope $scope) use (&$fileErrors, &$fileDependencies, &$exportedNodes, $file, $registry, $outerNodeCallback, $analysedFiles, &$linesToIgnore, &$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]); + } + } if ($outerNodeCallback !== null) { $outerNodeCallback($node, $scope); } @@ -80,7 +90,7 @@ public function analyseFile( foreach ($registry->getRules($nodeType) as $rule) { try { $ruleErrors = $rule->processNode($node, $scope); - } catch (\PHPStan\AnalysedCodeException $e) { + } catch (AnalysedCodeException $e) { if (isset($uniquedAnalysedCodeExceptionMessages[$e->getMessage()])) { continue; } @@ -108,7 +118,7 @@ public function analyseFile( $metadata = []; if ($scope->isInTrait()) { $traitReflection = $scope->getTraitReflection(); - if ($traitReflection->getFileName() !== false) { + if ($traitReflection->getFileName() !== null) { $traitFilePath = $traitReflection->getFileName(); } } @@ -158,13 +168,21 @@ public function analyseFile( $nodeLine, $nodeType, $identifier, - $metadata + $metadata, ); } } - foreach ($this->getLinesToIgnore($node) as $lineToIgnore) { - $linesToIgnore[$scope->getFileDescription()][$lineToIgnore] = true; + 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]); + } } try { @@ -175,11 +193,11 @@ public function analyseFile( if ($dependencies->getExportedNode() !== null) { $exportedNodes[] = $dependencies->getExportedNode(); } - } catch (\PHPStan\AnalysedCodeException $e) { + } catch (AnalysedCodeException) { // pass - } catch (IdentifierNotFound $e) { + } catch (IdentifierNotFound) { // pass - } catch (UnableToCompileNode | NotAClassReflection | NotAnInterfaceReflection $e) { + } catch (UnableToCompileNode | NotAClassReflection | NotAnInterfaceReflection) { // pass } }; @@ -189,7 +207,7 @@ public function analyseFile( $this->nodeScopeResolver->processNodes( $parserNodes, $scope, - $nodeCallback + $nodeCallback, ); $unmatchedLineIgnores = $linesToIgnore; foreach ($temporaryFileErrors as $tmpFileError) { @@ -224,18 +242,18 @@ public function analyseFile( null, null, null, - 'ignoredError.unmatchedOnLine' + 'ignoredError.unmatchedOnLine', ); } } } } catch (\PhpParser\Error $e) { $fileErrors[] = new Error($e->getMessage(), $file, $e->getStartLine() !== -1 ? $e->getStartLine() : null, $e); - } catch (\PHPStan\Parser\ParserErrorsException $e) { + } catch (ParserErrorsException $e) { foreach ($e->getErrors() as $error) { $fileErrors[] = new Error($error->getMessage(), $e->getParsedFile() ?? $file, $error->getStartLine() !== -1 ? $error->getStartLine() : null, $e); } - } catch (\PHPStan\AnalysedCodeException $e) { + } catch (AnalysedCodeException $e) { $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'); @@ -248,11 +266,14 @@ public function analyseFile( $fileErrors[] = new Error(sprintf('File %s does not exist.', $file), $file, null, false); } + $this->restoreCollectErrorsHandler(); + + $fileErrors = array_merge($fileErrors, $this->collectedErrors); + return new FileAnalyserResult($fileErrors, array_values(array_unique($fileDependencies)), $exportedNodes); } /** - * @param Node $node * @return int[] */ private function getLinesToIgnore(Node $node): array @@ -277,6 +298,26 @@ private function getLinesToIgnore(Node $node): array return $lines; } + /** + * @param Node[] $nodes + * @return array> + */ + private function getLinesToIgnoreFromTokens(string $file, array $nodes): array + { + if (!isset($nodes[0])) { + return []; + } + + /** @var int[] $tokenLines */ + $tokenLines = $nodes[0]->getAttribute('linesToIgnore', []); + $lines = []; + foreach ($tokenLines as $tokenLine) { + $lines[$file][$tokenLine] = true; + } + + return $lines; + } + private function findLineToIgnoreComment(Comment $comment): ?int { $text = $comment->getText(); @@ -300,4 +341,35 @@ private function findLineToIgnoreComment(Comment $comment): ?int return null; } + /** + * @param array $analysedFiles + */ + private function collectErrors(array $analysedFiles): void + { + $this->collectedErrors = []; + set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) use ($analysedFiles): bool { + if ((error_reporting() & $errno) === 0) { + // silence @ operator + return true; + } + + if ($errno === E_DEPRECATED) { + return true; + } + + if (!isset($analysedFiles[$errfile])) { + return true; + } + + $this->collectedErrors[] = new Error($errstr, $errfile, $errline, true); + + return true; + }); + } + + private function restoreCollectErrorsHandler(): void + { + restore_error_handler(); + } + } diff --git a/src/Analyser/FileAnalyserResult.php b/src/Analyser/FileAnalyserResult.php index 024a953026..9669866ef8 100644 --- a/src/Analyser/FileAnalyserResult.php +++ b/src/Analyser/FileAnalyserResult.php @@ -7,25 +7,13 @@ class FileAnalyserResult { - /** @var Error[] */ - private array $errors; - - /** @var array */ - private array $dependencies; - - /** @var array */ - private array $exportedNodes; - /** * @param Error[] $errors * @param array $dependencies * @param array $exportedNodes */ - public function __construct(array $errors, array $dependencies, array $exportedNodes) + public function __construct(private array $errors, private array $dependencies, private array $exportedNodes) { - $this->errors = $errors; - $this->dependencies = $dependencies; - $this->exportedNodes = $exportedNodes; } /** diff --git a/src/Analyser/IgnoredError.php b/src/Analyser/IgnoredError.php index f683a95ee1..ab3c1faf6c 100644 --- a/src/Analyser/IgnoredError.php +++ b/src/Analyser/IgnoredError.php @@ -2,14 +2,20 @@ namespace PHPStan\Analyser; +use Nette\Utils\Strings; use PHPStan\File\FileExcluder; use PHPStan\File\FileHelper; +use function count; +use function implode; +use function is_array; +use function preg_quote; +use function sprintf; +use function str_replace; class IgnoredError { /** - * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param mixed[]|string $ignoredError * @return string Representation of the ignored error */ @@ -34,18 +40,13 @@ public static function stringifyPattern($ignoredError): string } /** - * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint - * @param FileHelper $fileHelper - * @param Error $error - * @param string $ignoredErrorPattern - * @param string|null $path * @return bool To ignore or not to ignore? */ public static function shouldIgnore( FileHelper $fileHelper, Error $error, string $ignoredErrorPattern, - ?string $path + ?string $path, ): bool { // normalize newlines to allow working with ignore-patterns independent of used OS newline-format @@ -54,12 +55,11 @@ public static function shouldIgnore( $ignoredErrorPattern = str_replace([preg_quote('\r\n'), preg_quote('\r')], preg_quote('\n'), $ignoredErrorPattern); if ($path !== null) { - $fileExcluder = new FileExcluder($fileHelper, [$path], []); - - if (\Nette\Utils\Strings::match($errorMessage, $ignoredErrorPattern) === null) { + if (Strings::match($errorMessage, $ignoredErrorPattern) === null) { return false; } + $fileExcluder = new FileExcluder($fileHelper, [$path], []); $isExcluded = $fileExcluder->isExcludedFromAnalysing($error->getFilePath()); if (!$isExcluded && $error->getTraitFilePath() !== null) { return $fileExcluder->isExcludedFromAnalysing($error->getTraitFilePath()); @@ -68,7 +68,7 @@ public static function shouldIgnore( return $isExcluded; } - return \Nette\Utils\Strings::match($errorMessage, $ignoredErrorPattern) !== null; + return Strings::match($errorMessage, $ignoredErrorPattern) !== null; } } diff --git a/src/Analyser/IgnoredErrorHelper.php b/src/Analyser/IgnoredErrorHelper.php index cf57521f3b..b683886b97 100644 --- a/src/Analyser/IgnoredErrorHelper.php +++ b/src/Analyser/IgnoredErrorHelper.php @@ -3,45 +3,30 @@ namespace PHPStan\Analyser; use Nette\Utils\Json; -use PHPStan\Command\IgnoredRegexValidator; +use Nette\Utils\JsonException; use PHPStan\File\FileHelper; +use function is_array; +use function is_file; +use function sprintf; class IgnoredErrorHelper { - private IgnoredRegexValidator $ignoredRegexValidator; - - private \PHPStan\File\FileHelper $fileHelper; - - /** @var (string|mixed[])[] */ - private array $ignoreErrors; - - private bool $reportUnmatchedIgnoredErrors; - /** - * @param IgnoredRegexValidator $ignoredRegexValidator - * @param FileHelper $fileHelper * @param (string|mixed[])[] $ignoreErrors - * @param bool $reportUnmatchedIgnoredErrors */ public function __construct( - IgnoredRegexValidator $ignoredRegexValidator, - FileHelper $fileHelper, - array $ignoreErrors, - bool $reportUnmatchedIgnoredErrors + private FileHelper $fileHelper, + private array $ignoreErrors, + private bool $reportUnmatchedIgnoredErrors, ) { - $this->ignoredRegexValidator = $ignoredRegexValidator; - $this->fileHelper = $fileHelper; - $this->ignoreErrors = $ignoreErrors; - $this->reportUnmatchedIgnoredErrors = $reportUnmatchedIgnoredErrors; } public function initialize(): IgnoredErrorHelperResult { $otherIgnoreErrors = []; $ignoreErrorsByFile = []; - $warnings = []; $errors = []; foreach ($this->ignoreErrors as $i => $ignoreError) { try { @@ -49,7 +34,7 @@ public function initialize(): IgnoredErrorHelperResult if (!isset($ignoreError['message'])) { $errors[] = sprintf( 'Ignored error %s is missing a message.', - Json::encode($ignoreError) + Json::encode($ignoreError), ); continue; } @@ -57,7 +42,7 @@ public function initialize(): IgnoredErrorHelperResult if (!isset($ignoreError['paths'])) { $errors[] = sprintf( 'Ignored error %s is missing a path.', - Json::encode($ignoreError) + Json::encode($ignoreError), ); } @@ -80,78 +65,18 @@ public function initialize(): IgnoredErrorHelperResult 'ignoreError' => $ignoreError, ]; } - - $ignoreMessage = $ignoreError['message']; - \Nette\Utils\Strings::match('', $ignoreMessage); - if (isset($ignoreError['count'])) { - continue; // ignoreError coming from baseline will be correct - } - $validationResult = $this->ignoredRegexValidator->validate($ignoreMessage); - $ignoredTypes = $validationResult->getIgnoredTypes(); - if (count($ignoredTypes) > 0) { - $warnings[] = $this->createIgnoredTypesWarning($ignoreMessage, $ignoredTypes); - } - - if ($validationResult->hasAnchorsInTheMiddle()) { - $warnings[] = $this->createAnchorInTheMiddleWarning($ignoreMessage); - } - - if ($validationResult->areAllErrorsIgnored()) { - $errors[] = sprintf("Ignored error %s has an unescaped '%s' which leads to ignoring all errors. Use '%s' instead.", $ignoreMessage, $validationResult->getWrongSequence(), $validationResult->getEscapedWrongSequence()); - } } else { $otherIgnoreErrors[] = [ 'index' => $i, 'ignoreError' => $ignoreError, ]; - $ignoreMessage = $ignoreError; - \Nette\Utils\Strings::match('', $ignoreMessage); - $validationResult = $this->ignoredRegexValidator->validate($ignoreMessage); - $ignoredTypes = $validationResult->getIgnoredTypes(); - if (count($ignoredTypes) > 0) { - $warnings[] = $this->createIgnoredTypesWarning($ignoreMessage, $ignoredTypes); - } - - if ($validationResult->hasAnchorsInTheMiddle()) { - $warnings[] = $this->createAnchorInTheMiddleWarning($ignoreMessage); - } - - if ($validationResult->areAllErrorsIgnored()) { - $errors[] = sprintf("Ignored error %s has an unescaped '%s' which leads to ignoring all errors. Use '%s' instead.", $ignoreMessage, $validationResult->getWrongSequence(), $validationResult->getEscapedWrongSequence()); - } } - } catch (\Nette\Utils\RegexpException $e) { - $errors[] = $e->getMessage(); - } catch (\Nette\Utils\JsonException $e) { + } catch (JsonException $e) { $errors[] = $e->getMessage(); } } - return new IgnoredErrorHelperResult($this->fileHelper, $errors, $warnings, $otherIgnoreErrors, $ignoreErrorsByFile, $this->ignoreErrors, $this->reportUnmatchedIgnoredErrors); - } - - /** - * @param string $regex - * @param array $ignoredTypes - * @return string - */ - private function createIgnoredTypesWarning(string $regex, array $ignoredTypes): string - { - return sprintf( - "Ignored error %s has an unescaped '|' which leads to ignoring more errors than intended. Use '\\|' instead.\n%s", - $regex, - sprintf( - "It ignores all errors containing the following types:\n%s", - implode("\n", array_map(static function (string $typeDescription): string { - return sprintf('* %s', $typeDescription); - }, array_keys($ignoredTypes))) - ) - ); - } - - private function createAnchorInTheMiddleWarning(string $regex): string - { - return sprintf("Ignored error %s has an unescaped anchor '$' in the middle. This leads to unintended behavior. Use '\\$' instead.", $regex); + return new IgnoredErrorHelperResult($this->fileHelper, $errors, $otherIgnoreErrors, $ignoreErrorsByFile, $this->ignoreErrors, $this->reportUnmatchedIgnoredErrors); } } diff --git a/src/Analyser/IgnoredErrorHelperResult.php b/src/Analyser/IgnoredErrorHelperResult.php index 4aa7b42bf1..e0dc5de4f6 100644 --- a/src/Analyser/IgnoredErrorHelperResult.php +++ b/src/Analyser/IgnoredErrorHelperResult.php @@ -3,55 +3,35 @@ namespace PHPStan\Analyser; use PHPStan\File\FileHelper; +use PHPStan\ShouldNotHappenException; +use function array_fill_keys; +use function array_filter; +use function array_key_exists; +use function array_merge; +use function array_values; +use function count; +use function is_array; +use function is_string; +use function sprintf; class IgnoredErrorHelperResult { - private FileHelper $fileHelper; - - /** @var string[] */ - private array $errors; - - /** @var string[] */ - private array $warnings; - - /** @var array> */ - private array $otherIgnoreErrors; - - /** @var array>> */ - private array $ignoreErrorsByFile; - - /** @var (string|mixed[])[] */ - private array $ignoreErrors; - - private bool $reportUnmatchedIgnoredErrors; - /** - * @param FileHelper $fileHelper * @param string[] $errors - * @param string[] $warnings * @param array> $otherIgnoreErrors * @param array>> $ignoreErrorsByFile * @param (string|mixed[])[] $ignoreErrors - * @param bool $reportUnmatchedIgnoredErrors */ public function __construct( - FileHelper $fileHelper, - array $errors, - array $warnings, - array $otherIgnoreErrors, - array $ignoreErrorsByFile, - array $ignoreErrors, - bool $reportUnmatchedIgnoredErrors + private FileHelper $fileHelper, + private array $errors, + private array $otherIgnoreErrors, + private array $ignoreErrorsByFile, + private array $ignoreErrors, + private bool $reportUnmatchedIgnoredErrors, ) { - $this->fileHelper = $fileHelper; - $this->errors = $errors; - $this->warnings = $warnings; - $this->otherIgnoreErrors = $otherIgnoreErrors; - $this->ignoreErrorsByFile = $ignoreErrorsByFile; - $this->ignoreErrors = $ignoreErrors; - $this->reportUnmatchedIgnoredErrors = $reportUnmatchedIgnoredErrors; } /** @@ -62,14 +42,6 @@ public function getErrors(): array return $this->errors; } - /** - * @return string[] - */ - public function getWarnings(): array - { - return $this->warnings; - } - /** * @param Error[] $errors * @param string[] $analysedFiles @@ -79,7 +51,7 @@ public function process( array $errors, bool $onlyFiles, array $analysedFiles, - bool $hasInternalErrors + bool $hasInternalErrors, ): array { $unmatchedIgnoredErrors = $this->ignoreErrors; @@ -122,7 +94,7 @@ public function process( if (isset($unmatchedIgnoredErrors[$i])) { if (!is_array($unmatchedIgnoredErrors[$i])) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } unset($unmatchedIgnoredErrors[$i]['paths'][$j]); if (isset($unmatchedIgnoredErrors[$i]['paths']) && count($unmatchedIgnoredErrors[$i]['paths']) === 0) { @@ -132,7 +104,7 @@ public function process( break; } } else { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } } @@ -140,7 +112,7 @@ public function process( if (!$error->canBeIgnored()) { $addErrors[] = sprintf( 'Error message "%s" cannot be ignored, use excludePaths instead.', - $error->getMessage() + $error->getMessage(), ); return true; } @@ -206,7 +178,7 @@ public function process( $unmatchedIgnoredError['count'], $unmatchedIgnoredError['count'] === 1 ? 'time' : 'times', $unmatchedIgnoredError['realCount'], - $unmatchedIgnoredError['realCount'] === 1 ? 'time' : 'times' + $unmatchedIgnoredError['realCount'] === 1 ? 'time' : 'times', ), $unmatchedIgnoredError['file'], $unmatchedIgnoredError['line'], false); } @@ -228,7 +200,7 @@ public function process( $unmatchedIgnoredError['count'], $unmatchedIgnoredError['count'] === 1 ? 'time' : 'times', $unmatchedIgnoredError['realCount'], - $unmatchedIgnoredError['realCount'] === 1 ? 'time' : 'times' + $unmatchedIgnoredError['realCount'] === 1 ? 'time' : 'times', ), $unmatchedIgnoredError['file'], $unmatchedIgnoredError['line'], false); } } elseif (isset($unmatchedIgnoredError['realPath'])) { @@ -239,16 +211,16 @@ public function process( $errors[] = new Error( sprintf( 'Ignored error pattern %s was not matched in reported errors.', - IgnoredError::stringifyPattern($unmatchedIgnoredError) + IgnoredError::stringifyPattern($unmatchedIgnoredError), ), $unmatchedIgnoredError['realPath'], null, - false + false, ); } elseif (!$onlyFiles) { $errors[] = sprintf( 'Ignored error pattern %s was not matched in reported errors.', - IgnoredError::stringifyPattern($unmatchedIgnoredError) + IgnoredError::stringifyPattern($unmatchedIgnoredError), ); } } diff --git a/src/Analyser/LazyScopeFactory.php b/src/Analyser/LazyScopeFactory.php index d9ac4ca85b..e704d36419 100644 --- a/src/Analyser/LazyScopeFactory.php +++ b/src/Analyser/LazyScopeFactory.php @@ -6,62 +6,51 @@ use PHPStan\DependencyInjection\Container; use PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider; use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider; +use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Type; +use function is_a; class LazyScopeFactory implements ScopeFactory { - private string $scopeClass; - - private Container $container; - /** @var string[] */ private array $dynamicConstantNames; private bool $treatPhpDocTypesAsCertain; - private bool $objectFromNewClass; + private bool $explicitMixedInUnknownGenericNew; public function __construct( - string $scopeClass, - Container $container + private string $scopeClass, + private Container $container, ) { - $this->scopeClass = $scopeClass; - $this->container = $container; $this->dynamicConstantNames = $container->getParameter('dynamicConstantNames'); $this->treatPhpDocTypesAsCertain = $container->getParameter('treatPhpDocTypesAsCertain'); - $this->objectFromNewClass = $container->getParameter('featureToggles')['objectFromNewClass']; + $this->explicitMixedInUnknownGenericNew = $this->container->getParameter('featureToggles')['explicitMixedInUnknownGenericNew']; } /** - * @param \PHPStan\Analyser\ScopeContext $context - * @param bool $declareStrictTypes * @param array $constantTypes - * @param \PHPStan\Reflection\FunctionReflection|\PHPStan\Reflection\MethodReflection|null $function - * @param string|null $namespace - * @param \PHPStan\Analyser\VariableTypeHolder[] $variablesTypes - * @param \PHPStan\Analyser\VariableTypeHolder[] $moreSpecificTypes + * @param VariableTypeHolder[] $variablesTypes + * @param VariableTypeHolder[] $moreSpecificTypes * @param array $conditionalExpressions - * @param string|null $inClosureBindScopeClass - * @param \PHPStan\Reflection\ParametersAcceptor|null $anonymousFunctionReflection - * @param bool $inFirstLevelStatement * @param array $currentlyAssignedExpressions * @param array $nativeExpressionTypes - * @param array<\PHPStan\Reflection\FunctionReflection|\PHPStan\Reflection\MethodReflection> $inFunctionCallsStack - * @param bool $afterExtractCall - * @param Scope|null $parentScope + * @param array<(FunctionReflection|MethodReflection)> $inFunctionCallsStack * - * @return MutatingScope */ public function create( ScopeContext $context, bool $declareStrictTypes = false, array $constantTypes = [], - $function = null, + FunctionReflection|MethodReflection|null $function = null, ?string $namespace = null, array $variablesTypes = [], array $moreSpecificTypes = [], @@ -73,12 +62,12 @@ public function create( array $nativeExpressionTypes = [], array $inFunctionCallsStack = [], bool $afterExtractCall = false, - ?Scope $parentScope = null + ?Scope $parentScope = null, ): MutatingScope { $scopeClass = $this->scopeClass; if (!is_a($scopeClass, MutatingScope::class, true)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return new $scopeClass( @@ -89,9 +78,10 @@ public function create( $this->container->getByType(Standard::class), $this->container->getByType(TypeSpecifier::class), $this->container->getByType(PropertyReflectionFinder::class), - $this->container->getByType(\PHPStan\Parser\Parser::class), + $this->container->getService('currentPhpVersionSimpleParser'), $this->container->getByType(NodeScopeResolver::class), $context, + $this->container->getByType(PhpVersion::class), $declareStrictTypes, $constantTypes, $function, @@ -107,9 +97,9 @@ public function create( $inFunctionCallsStack, $this->dynamicConstantNames, $this->treatPhpDocTypesAsCertain, - $this->objectFromNewClass, $afterExtractCall, - $parentScope + $parentScope, + $this->explicitMixedInUnknownGenericNew, ); } diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index c0c03073e0..acb13d5b92 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2,6 +2,9 @@ namespace PHPStan\Analyser; +use ArrayAccess; +use Closure; +use Generator; use Nette\Utils\Strings; use PhpParser\Node; use PhpParser\Node\Arg; @@ -25,8 +28,17 @@ use PhpParser\Node\Scalar\EncapsedStringPart; use PhpParser\Node\Scalar\LNumber; use PhpParser\Node\Scalar\String_; +use PhpParser\NodeFinder; +use PhpParser\PrettyPrinter\Standard; use PHPStan\Node\ExecutionEndNode; +use PHPStan\Node\Expr\GetIterableValueTypeExpr; +use PHPStan\Node\Expr\GetOffsetValueTypeExpr; +use PHPStan\Node\Expr\OriginalPropertyTypeExpr; +use PHPStan\Node\Expr\SetOffsetValueTypeExpr; use PHPStan\Parser\Parser; +use PHPStan\Parser\ParserErrorsException; +use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ConstantReflection; @@ -34,6 +46,7 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\Native\NativeParameterReflection; +use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\PassedByReference; @@ -44,7 +57,11 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\BooleanType; @@ -60,8 +77,10 @@ use PHPStan\Type\ConstantType; use PHPStan\Type\ConstantTypeHelper; use PHPStan\Type\DynamicReturnTypeExtensionRegistry; +use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\ErrorType; use PHPStan\Type\FloatType; +use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateType; @@ -92,11 +111,37 @@ use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use PHPStan\Type\VoidType; +use Throwable; +use function abs; +use function array_column; +use function array_filter; use function array_key_exists; +use function array_keys; +use function array_map; +use function array_pop; +use function count; +use function dirname; +use function get_class; +use function in_array; +use function is_float; +use function is_string; +use function ltrim; +use function max; +use function sprintf; +use function str_starts_with; +use function strlen; +use function strtolower; +use function substr; +use function usort; +use const PHP_INT_MAX; +use const PHP_INT_MIN; +use const PHP_INT_SIZE; class MutatingScope implements Scope { + public const CALCULATE_SCALARS_LIMIT = 128; + private const OPERATOR_SIGIL_MAP = [ Node\Expr\AssignOp\Plus::class => '+', Node\Expr\AssignOp\Minus::class => '-', @@ -105,167 +150,64 @@ class MutatingScope implements Scope Node\Expr\AssignOp\Div::class => '/', ]; - private \PHPStan\Analyser\ScopeFactory $scopeFactory; - - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Type\DynamicReturnTypeExtensionRegistry $dynamicReturnTypeExtensionRegistry; - - private OperatorTypeSpecifyingExtensionRegistry $operatorTypeSpecifyingExtensionRegistry; - - private \PhpParser\PrettyPrinter\Standard $printer; - - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; - - private \PHPStan\Rules\Properties\PropertyReflectionFinder $propertyReflectionFinder; - - private Parser $parser; - - private NodeScopeResolver $nodeScopeResolver; - - private \PHPStan\Analyser\ScopeContext $context; - - /** @var \PHPStan\Type\Type[] */ + /** @var Type[] */ private array $resolvedTypes = []; - private bool $declareStrictTypes; - - /** @var array */ - private array $constantTypes; + /** @var array */ + private array $truthyScopes = []; - /** @var \PHPStan\Reflection\FunctionReflection|MethodReflection|null */ - private $function; + /** @var array */ + private array $falseyScopes = []; private ?string $namespace; - /** @var \PHPStan\Analyser\VariableTypeHolder[] */ - private array $variableTypes; - - /** @var \PHPStan\Analyser\VariableTypeHolder[] */ - private array $moreSpecificTypes; - - /** @var array */ - private array $conditionalExpressions; - - private ?string $inClosureBindScopeClass; - - private ?ParametersAcceptor $anonymousFunctionReflection; - - private bool $inFirstLevelStatement; - - /** @var array */ - private array $currentlyAssignedExpressions; - - /** @var array */ - private array $inFunctionCallsStack; - - /** @var array */ - private array $nativeExpressionTypes; - - /** @var string[] */ - private array $dynamicConstantNames; - - private bool $treatPhpDocTypesAsCertain; - - private bool $objectFromNewClass; - - private bool $afterExtractCall; - - private ?Scope $parentScope; - /** - * @param \PHPStan\Analyser\ScopeFactory $scopeFactory - * @param ReflectionProvider $reflectionProvider - * @param \PHPStan\Type\DynamicReturnTypeExtensionRegistry $dynamicReturnTypeExtensionRegistry - * @param \PHPStan\Type\OperatorTypeSpecifyingExtensionRegistry $operatorTypeSpecifyingExtensionRegistry - * @param \PhpParser\PrettyPrinter\Standard $printer - * @param \PHPStan\Analyser\TypeSpecifier $typeSpecifier - * @param \PHPStan\Rules\Properties\PropertyReflectionFinder $propertyReflectionFinder - * @param Parser $parser - * @param NodeScopeResolver $nodeScopeResolver - * @param \PHPStan\Analyser\ScopeContext $context - * @param bool $declareStrictTypes * @param array $constantTypes - * @param \PHPStan\Reflection\FunctionReflection|MethodReflection|null $function - * @param string|null $namespace - * @param \PHPStan\Analyser\VariableTypeHolder[] $variablesTypes - * @param \PHPStan\Analyser\VariableTypeHolder[] $moreSpecificTypes + * @param VariableTypeHolder[] $variableTypes + * @param VariableTypeHolder[] $moreSpecificTypes * @param array $conditionalExpressions - * @param string|null $inClosureBindScopeClass - * @param \PHPStan\Reflection\ParametersAcceptor|null $anonymousFunctionReflection - * @param bool $inFirstLevelStatement * @param array $currentlyAssignedExpressions * @param array $nativeExpressionTypes * @param array $inFunctionCallsStack * @param string[] $dynamicConstantNames - * @param bool $treatPhpDocTypesAsCertain - * @param bool $objectFromNewClass - * @param bool $afterExtractCall - * @param Scope|null $parentScope */ public function __construct( - ScopeFactory $scopeFactory, - ReflectionProvider $reflectionProvider, - DynamicReturnTypeExtensionRegistry $dynamicReturnTypeExtensionRegistry, - OperatorTypeSpecifyingExtensionRegistry $operatorTypeSpecifyingExtensionRegistry, - \PhpParser\PrettyPrinter\Standard $printer, - TypeSpecifier $typeSpecifier, - PropertyReflectionFinder $propertyReflectionFinder, - Parser $parser, - NodeScopeResolver $nodeScopeResolver, - ScopeContext $context, - bool $declareStrictTypes = false, - array $constantTypes = [], - $function = null, + private ScopeFactory $scopeFactory, + private ReflectionProvider $reflectionProvider, + private DynamicReturnTypeExtensionRegistry $dynamicReturnTypeExtensionRegistry, + private OperatorTypeSpecifyingExtensionRegistry $operatorTypeSpecifyingExtensionRegistry, + private Standard $printer, + private TypeSpecifier $typeSpecifier, + private PropertyReflectionFinder $propertyReflectionFinder, + private Parser $parser, + private NodeScopeResolver $nodeScopeResolver, + private ScopeContext $context, + private PhpVersion $phpVersion, + private bool $declareStrictTypes = false, + private array $constantTypes = [], + private FunctionReflection|MethodReflection|null $function = null, ?string $namespace = null, - array $variablesTypes = [], - array $moreSpecificTypes = [], - array $conditionalExpressions = [], - ?string $inClosureBindScopeClass = null, - ?ParametersAcceptor $anonymousFunctionReflection = null, - bool $inFirstLevelStatement = true, - array $currentlyAssignedExpressions = [], - array $nativeExpressionTypes = [], - array $inFunctionCallsStack = [], - array $dynamicConstantNames = [], - bool $treatPhpDocTypesAsCertain = true, - bool $objectFromNewClass = false, - bool $afterExtractCall = false, - ?Scope $parentScope = null + private array $variableTypes = [], + private array $moreSpecificTypes = [], + private array $conditionalExpressions = [], + private ?string $inClosureBindScopeClass = null, + private ?ParametersAcceptor $anonymousFunctionReflection = null, + private bool $inFirstLevelStatement = true, + private array $currentlyAssignedExpressions = [], + private array $nativeExpressionTypes = [], + private array $inFunctionCallsStack = [], + private array $dynamicConstantNames = [], + private bool $treatPhpDocTypesAsCertain = true, + private bool $afterExtractCall = false, + private ?Scope $parentScope = null, + private bool $explicitMixedInUnknownGenericNew = false, ) { if ($namespace === '') { $namespace = null; } - $this->scopeFactory = $scopeFactory; - $this->reflectionProvider = $reflectionProvider; - $this->dynamicReturnTypeExtensionRegistry = $dynamicReturnTypeExtensionRegistry; - $this->operatorTypeSpecifyingExtensionRegistry = $operatorTypeSpecifyingExtensionRegistry; - $this->printer = $printer; - $this->typeSpecifier = $typeSpecifier; - $this->propertyReflectionFinder = $propertyReflectionFinder; - $this->parser = $parser; - $this->nodeScopeResolver = $nodeScopeResolver; - $this->context = $context; - $this->declareStrictTypes = $declareStrictTypes; - $this->constantTypes = $constantTypes; - $this->function = $function; $this->namespace = $namespace; - $this->variableTypes = $variablesTypes; - $this->moreSpecificTypes = $moreSpecificTypes; - $this->conditionalExpressions = $conditionalExpressions; - $this->inClosureBindScopeClass = $inClosureBindScopeClass; - $this->anonymousFunctionReflection = $anonymousFunctionReflection; - $this->inFirstLevelStatement = $inFirstLevelStatement; - $this->currentlyAssignedExpressions = $currentlyAssignedExpressions; - $this->nativeExpressionTypes = $nativeExpressionTypes; - $this->inFunctionCallsStack = $inFunctionCallsStack; - $this->dynamicConstantNames = $dynamicConstantNames; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; - $this->objectFromNewClass = $objectFromNewClass; - $this->afterExtractCall = $afterExtractCall; - $this->parentScope = $parentScope; } /** @api */ @@ -284,20 +226,20 @@ public function getFileDescription(): string /** @var ClassReflection $classReflection */ $classReflection = $this->context->getClassReflection(); - $className = sprintf('class %s', $classReflection->getDisplayName()); - if ($classReflection->isAnonymous()) { - $className = 'anonymous class'; + $className = $classReflection->getDisplayName(); + if (!$classReflection->isAnonymous()) { + $className = sprintf('class %s', $className); } $traitReflection = $this->context->getTraitReflection(); - if ($traitReflection->getFileName() === false) { - throw new \PHPStan\ShouldNotHappenException(); + if ($traitReflection->getFileName() === null) { + throw new ShouldNotHappenException(); } return sprintf( '%s (in context of %s)', $traitReflection->getFileName(), - $className + $className, ); } @@ -315,7 +257,7 @@ public function enterDeclareStrictTypes(): self [], null, null, - $this->variableTypes + $this->variableTypes, ); } @@ -345,7 +287,7 @@ public function getTraitReflection(): ?ClassReflection /** * @api - * @return \PHPStan\Reflection\FunctionReflection|\PHPStan\Reflection\MethodReflection|null + * @return FunctionReflection|MethodReflection|null */ public function getFunction() { @@ -371,7 +313,7 @@ public function getParentScope(): ?Scope } /** - * @return array + * @return array */ private function getVariableTypes(): array { @@ -402,7 +344,7 @@ public function afterExtractCall(): self $this->nativeExpressionTypes, $this->inFunctionCallsStack, true, - $this->parentScope + $this->parentScope, ); } @@ -433,7 +375,7 @@ public function afterClearstatcacheCall(): self 'filetype', 'fileperms', ] as $functionName) { - if (!Strings::startsWith((string) $exprString, $functionName . '(') && !Strings::startsWith((string) $exprString, '\\' . $functionName . '(')) { + if (!str_starts_with((string) $exprString, $functionName . '(') && !str_starts_with((string) $exprString, '\\' . $functionName . '(')) { continue; } @@ -457,7 +399,7 @@ public function afterClearstatcacheCall(): self $this->nativeExpressionTypes, $this->inFunctionCallsStack, $this->afterExtractCall, - $this->parentScope + $this->parentScope, ); } @@ -487,7 +429,7 @@ public function getVariableType(string $variableName): Type } if ($this->hasVariableType($variableName)->no()) { - throw new \PHPStan\Analyser\UndefinedVariableException($this, $variableName); + throw new UndefinedVariableException($this, $variableName); } if (!array_key_exists($variableName, $this->variableTypes)) { @@ -568,7 +510,7 @@ private function fileHasCompilerHaltStatementCalls(): bool { $nodes = $this->parser->parseFile($this->getFile()); foreach ($nodes as $node) { - if ($node instanceof \PhpParser\Node\Stmt\HaltCompiler) { + if ($node instanceof Node\Stmt\HaltCompiler) { return true; } } @@ -589,7 +531,7 @@ public function getAnonymousFunctionReflection(): ?ParametersAcceptor } /** @api */ - public function getAnonymousFunctionReturnType(): ?\PHPStan\Type\Type + public function getAnonymousFunctionReturnType(): ?Type { if ($this->anonymousFunctionReflection === null) { return null; @@ -601,6 +543,27 @@ public function getAnonymousFunctionReturnType(): ?\PHPStan\Type\Type /** @api */ public function getType(Expr $node): Type { + if ($node instanceof GetIterableValueTypeExpr) { + return $this->getType($node->getExpr())->getIterableValueType(); + } + if ($node instanceof GetOffsetValueTypeExpr) { + return $this->getType($node->getVar())->getOffsetValueType($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 OriginalPropertyTypeExpr) { + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($node->getPropertyFetch(), $this); + if ($propertyReflection === null) { + return new ErrorType(); + } + + return $propertyReflection->getReadableType(); + } + $key = $this->getNodeKey($node); if (!array_key_exists($key, $this->resolvedTypes)) { @@ -646,54 +609,123 @@ private function resolveType(Expr $node): Type if ( $node instanceof Expr\BinaryOp\Equal || $node instanceof Expr\BinaryOp\NotEqual - || $node instanceof Expr\Empty_ ) { + if ($node instanceof Expr\BinaryOp\Equal) { + if ( + $node->left instanceof Variable + && is_string($node->left->name) + && $node->right instanceof Variable + && is_string($node->right->name) + && $node->left->name === $node->right->name + ) { + return new ConstantBooleanType(true); + } + + $leftType = $this->getType($node->left); + $rightType = $this->getType($node->right); + + $stringType = new StringType(); + $integerType = new IntegerType(); + $floatType = new FloatType(); + if ( + ($stringType->isSuperTypeOf($leftType)->yes() && $stringType->isSuperTypeOf($rightType)->yes()) + || ($integerType->isSuperTypeOf($leftType)->yes() && $integerType->isSuperTypeOf($rightType)->yes()) + || ($floatType->isSuperTypeOf($leftType)->yes() && $floatType->isSuperTypeOf($rightType)->yes()) + ) { + return $this->getType(new Expr\BinaryOp\Identical($node->left, $node->right)); + } + } + + if ($node instanceof Expr\BinaryOp\NotEqual) { + if ( + $node->left instanceof Variable + && is_string($node->left->name) + && $node->right instanceof Variable + && is_string($node->right->name) + && $node->left->name === $node->right->name + ) { + return new ConstantBooleanType(false); + } + + $leftType = $this->getType($node->left); + $rightType = $this->getType($node->right); + + $stringType = new StringType(); + $integerType = new IntegerType(); + $floatType = new FloatType(); + if ( + ($stringType->isSuperTypeOf($leftType)->yes() && $stringType->isSuperTypeOf($rightType)->yes()) + || ($integerType->isSuperTypeOf($leftType)->yes() && $integerType->isSuperTypeOf($rightType)->yes()) + || ($floatType->isSuperTypeOf($leftType)->yes() && $floatType->isSuperTypeOf($rightType)->yes()) + ) { + return $this->getType(new Expr\BinaryOp\NotIdentical($node->left, $node->right)); + } + } + return new BooleanType(); } - if ($node instanceof Expr\Isset_) { - $result = new ConstantBooleanType(true); - foreach ($node->vars as $var) { - if ($var instanceof Expr\ArrayDimFetch && $var->dim !== null) { - $variableType = $this->getType($var->var); - $dimType = $this->getType($var->dim); - $hasOffset = $variableType->hasOffsetValueType($dimType); - $offsetValueType = $variableType->getOffsetValueType($dimType); - $offsetValueIsNotNull = (new NullType())->isSuperTypeOf($offsetValueType)->negate(); - $isset = $hasOffset->and($offsetValueIsNotNull)->toBooleanType(); - if ($isset instanceof ConstantBooleanType) { - if (!$isset->getValue()) { - return $isset; - } + 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()); + if ($isNull->maybe()) { + return null; + } + if ($isFalsey->maybe()) { + return null; + } - continue; + if ($isNull->yes()) { + if ($isFalsey->yes()) { + return false; + } + if ($isFalsey->no()) { + return true; } - $result = $isset; - continue; + return false; } - if ($var instanceof Expr\Variable && is_string($var->name)) { - $variableType = $this->getType($var); - $isNullSuperType = (new NullType())->isSuperTypeOf($variableType); - $has = $this->hasVariableType($var->name); - if ($has->no() || $isNullSuperType->yes()) { - return new ConstantBooleanType(false); + return !$isFalsey->yes(); + }); + if ($result === null) { + return new BooleanType(); + } + + return new ConstantBooleanType(!$result); + } + + if ($node instanceof Expr\Isset_) { + $issetResult = true; + foreach ($node->vars as $var) { + $result = $this->issetCheck($var, static function (Type $type): ?bool { + $isNull = (new NullType())->isSuperTypeOf($type); + if ($isNull->maybe()) { + return null; } - if ($has->maybe() || !$isNullSuperType->no()) { - $result = new BooleanType(); + return !$isNull->yes(); + }); + if ($result !== null) { + if (!$result) { + return new ConstantBooleanType($result); } + continue; } + $issetResult = $result; + } + + if ($issetResult === null) { return new BooleanType(); } - return $result; + return new ConstantBooleanType($issetResult); } - if ($node instanceof \PhpParser\Node\Expr\BooleanNot) { + if ($node instanceof Node\Expr\BooleanNot) { if ($this->treatPhpDocTypesAsCertain) { $exprBooleanType = $this->getType($node->expr)->toBoolean(); } else { @@ -706,7 +738,7 @@ private function resolveType(Expr $node): Type return new BooleanType(); } - if ($node instanceof \PhpParser\Node\Expr\BitwiseNot) { + if ($node instanceof Node\Expr\BitwiseNot) { $exprType = $this->getType($node->expr); return TypeTraverser::map($exprType, static function (Type $type, callable $traverse): Type { if ($type instanceof UnionType || $type instanceof IntersectionType) { @@ -726,8 +758,8 @@ private function resolveType(Expr $node): Type } if ( - $node instanceof \PhpParser\Node\Expr\BinaryOp\BooleanAnd - || $node instanceof \PhpParser\Node\Expr\BinaryOp\LogicalAnd + $node instanceof Node\Expr\BinaryOp\BooleanAnd + || $node instanceof Node\Expr\BinaryOp\LogicalAnd ) { if ($this->treatPhpDocTypesAsCertain) { $leftBooleanType = $this->getType($node->left)->toBoolean(); @@ -768,8 +800,8 @@ private function resolveType(Expr $node): Type } if ( - $node instanceof \PhpParser\Node\Expr\BinaryOp\BooleanOr - || $node instanceof \PhpParser\Node\Expr\BinaryOp\LogicalOr + $node instanceof Node\Expr\BinaryOp\BooleanOr + || $node instanceof Node\Expr\BinaryOp\LogicalOr ) { if ($this->treatPhpDocTypesAsCertain) { $leftBooleanType = $this->getType($node->left)->toBoolean(); @@ -808,7 +840,7 @@ private function resolveType(Expr $node): Type return new BooleanType(); } - if ($node instanceof \PhpParser\Node\Expr\BinaryOp\LogicalXor) { + if ($node instanceof Node\Expr\BinaryOp\LogicalXor) { if ($this->treatPhpDocTypesAsCertain) { $leftBooleanType = $this->getType($node->left)->toBoolean(); $rightBooleanType = $this->getType($node->right)->toBoolean(); @@ -822,7 +854,7 @@ private function resolveType(Expr $node): Type && $rightBooleanType instanceof ConstantBooleanType ) { return new ConstantBooleanType( - $leftBooleanType->getValue() xor $rightBooleanType->getValue() + $leftBooleanType->getValue() xor $rightBooleanType->getValue(), ); } @@ -830,6 +862,16 @@ private function resolveType(Expr $node): Type } if ($node instanceof Expr\BinaryOp\Identical) { + if ( + $node->left instanceof Variable + && is_string($node->left->name) + && $node->right instanceof Variable + && is_string($node->right->name) + && $node->left->name === $node->right->name + ) { + return new ConstantBooleanType(true); + } + if ($this->treatPhpDocTypesAsCertain) { $leftType = $this->getType($node->left); $rightType = $this->getType($node->right); @@ -876,6 +918,15 @@ private function resolveType(Expr $node): Type } if ($node instanceof Expr\BinaryOp\NotIdentical) { + if ( + $node->left instanceof Variable + && is_string($node->left->name) + && $node->right instanceof Variable + && is_string($node->right->name) + && $node->left->name === $node->right->name + ) { + return new ConstantBooleanType(false); + } if ($this->treatPhpDocTypesAsCertain) { $leftType = $this->getType($node->left); $rightType = $this->getType($node->right); @@ -999,6 +1050,7 @@ private function resolveType(Expr $node): Type if ($node instanceof Node\Expr\UnaryMinus) { $type = $this->getType($node->expr)->toNumber(); $scalarValues = TypeUtils::getConstantScalars($type); + if (count($scalarValues) > 0) { $newTypes = []; foreach ($scalarValues as $scalarValue) { @@ -1012,40 +1064,43 @@ private function resolveType(Expr $node): Type return TypeCombinator::union(...$newTypes); } + if ($type instanceof IntegerRangeType) { + return $this->resolveType(new Node\Expr\BinaryOp\Mul($node->expr, new LNumber(-1))); + } + return $type; } if ($node instanceof Expr\BinaryOp\Concat || $node instanceof Expr\AssignOp\Concat) { + return $this->resolveConcatType($node); + } + + if ( + $node instanceof Node\Expr\BinaryOp\Mul + || $node instanceof Node\Expr\AssignOp\Mul + ) { if ($node instanceof Node\Expr\AssignOp) { - $left = $node->var; - $right = $node->expr; + $leftType = $this->getType($node->var)->toNumber(); + $rightType = $this->getType($node->expr)->toNumber(); } else { - $left = $node->left; - $right = $node->right; + $leftType = $this->getType($node->left)->toNumber(); + $rightType = $this->getType($node->right)->toNumber(); } - $leftStringType = $this->getType($left)->toString(); - $rightStringType = $this->getType($right)->toString(); - if (TypeCombinator::union( - $leftStringType, - $rightStringType - ) instanceof ErrorType) { - return new ErrorType(); - } + $floatType = new FloatType(); - if ($leftStringType instanceof ConstantStringType && $leftStringType->getValue() === '') { - return $rightStringType; - } - - if ($rightStringType instanceof ConstantStringType && $rightStringType->getValue() === '') { - return $leftStringType; + if ($leftType instanceof ConstantIntegerType && $leftType->getValue() === 0) { + if ($floatType->isSuperTypeOf($rightType)->yes()) { + return new ConstantFloatType(0.0); + } + return new ConstantIntegerType(0); } - - if ($leftStringType instanceof ConstantStringType && $rightStringType instanceof ConstantStringType) { - return $leftStringType->append($rightStringType); + if ($rightType instanceof ConstantIntegerType && $rightType->getValue() === 0) { + if ($floatType->isSuperTypeOf($leftType)->yes()) { + return new ConstantFloatType(0.0); + } + return new ConstantIntegerType(0); } - - return new StringType(); } if ( @@ -1059,12 +1114,24 @@ private function resolveType(Expr $node): Type } else { $right = $node->right; } + $rightType = $this->getType($right); + + $integerType = $rightType->toInteger(); + if ( + $node instanceof Node\Expr\BinaryOp\Mod + || $node instanceof Node\Expr\AssignOp\Mod + ) { + if ($integerType instanceof ConstantIntegerType && $integerType->getValue() === 1) { + return new ConstantIntegerType(0); + } + } + + $rightScalarTypes = TypeUtils::getConstantScalars($rightType->toNumber()); + foreach ($rightScalarTypes as $scalarType) { - $rightTypes = TypeUtils::getConstantScalars($this->getType($right)->toNumber()); - foreach ($rightTypes as $rightType) { if ( - $rightType->getValue() === 0 - || $rightType->getValue() === 0.0 + $scalarType->getValue() === 0 + || $scalarType->getValue() === 0.0 ) { return new ErrorType(); } @@ -1088,11 +1155,18 @@ private function resolveType(Expr $node): Type $leftTypes = TypeUtils::getConstantScalars($this->getType($left)); $rightTypes = TypeUtils::getConstantScalars($this->getType($right)); - if (count($leftTypes) > 0 && count($rightTypes) > 0) { + $leftTypesCount = count($leftTypes); + $rightTypesCount = count($rightTypes); + if ($leftTypesCount > 0 && $rightTypesCount > 0) { $resultTypes = []; + $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT; foreach ($leftTypes as $leftType) { foreach ($rightTypes as $rightType) { - $resultTypes[] = $this->calculateFromScalars($node, $leftType, $rightType); + $resultType = $this->calculateFromScalars($node, $leftType, $rightType); + if ($generalize) { + $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); + } + $resultTypes[] = $resultType; } } return TypeCombinator::union(...$resultTypes); @@ -1100,6 +1174,52 @@ private function resolveType(Expr $node): Type } if ($node instanceof Node\Expr\BinaryOp\Mod || $node instanceof Expr\AssignOp\Mod) { + if ($node instanceof Node\Expr\AssignOp) { + $left = $node->var; + $right = $node->expr; + } else { + $left = $node->left; + $right = $node->right; + } + + $leftType = $this->getType($left); + $rightType = $this->getType($right); + + $integer = new IntegerType(); + $positiveInt = IntegerRangeType::fromInterval(0, null); + if ($integer->isSuperTypeOf($rightType)->yes()) { + $rangeMin = null; + $rangeMax = null; + + if ($rightType instanceof IntegerRangeType) { + $rangeMax = $rightType->getMax() !== null ? $rightType->getMax() - 1 : null; + } elseif ($rightType instanceof ConstantIntegerType) { + $rangeMax = $rightType->getValue() - 1; + } elseif ($rightType instanceof UnionType) { + foreach ($rightType->getTypes() as $type) { + if ($type instanceof IntegerRangeType) { + if ($type->getMax() === null) { + $rangeMax = null; + } else { + $rangeMax = max($rangeMax, $type->getMax()); + } + } elseif ($type instanceof ConstantIntegerType) { + $rangeMax = max($rangeMax, $type->getValue() - 1); + } + } + } + + if ($positiveInt->isSuperTypeOf($leftType)->yes()) { + $rangeMin = 0; + } elseif ($rangeMax !== null) { + $rangeMin = $rangeMax * -1; + } + + return IntegerRangeType::fromInterval($rangeMin, $rangeMax); + } elseif ($positiveInt->isSuperTypeOf($leftType)->yes()) { + return IntegerRangeType::fromInterval(0, null); + } + return new IntegerType(); } @@ -1127,7 +1247,7 @@ private function resolveType(Expr $node): Type if (TypeCombinator::union( $this->getType($left)->toNumber(), - $this->getType($right)->toNumber() + $this->getType($right)->toNumber(), ) instanceof ErrorType) { return new ErrorType(); } @@ -1189,31 +1309,6 @@ private function resolveType(Expr $node): Type $leftType = $this->getType($left); $rightType = $this->getType($right); - $operatorSigil = null; - - if ($node instanceof BinaryOp) { - $operatorSigil = $node->getOperatorSigil(); - } - - if ($operatorSigil === null) { - $operatorSigil = self::OPERATOR_SIGIL_MAP[get_class($node)] ?? null; - } - - if ($operatorSigil !== null) { - $operatorTypeSpecifyingExtensions = $this->operatorTypeSpecifyingExtensionRegistry->getOperatorTypeSpecifyingExtensions($operatorSigil, $leftType, $rightType); - - /** @var Type[] $extensionTypes */ - $extensionTypes = []; - - foreach ($operatorTypeSpecifyingExtensions as $extension) { - $extensionTypes[] = $extension->specifyType($operatorSigil, $leftType, $rightType); - } - - if (count($extensionTypes) > 0) { - return TypeCombinator::union(...$extensionTypes); - } - } - if ($node instanceof Expr\AssignOp\Plus || $node instanceof Expr\BinaryOp\Plus) { $leftConstantArrays = TypeUtils::getConstantArrays($leftType); $rightConstantArrays = TypeUtils::getConstantArrays($rightType); @@ -1226,7 +1321,7 @@ private function resolveType(Expr $node): Type foreach ($leftConstantArray->getKeyTypes() as $leftKeyType) { $newArrayBuilder->setOffsetValueType( $leftKeyType, - $leftConstantArray->getOffsetValueType($leftKeyType) + $leftConstantArray->getOffsetValueType($leftKeyType), ); } $resultTypes[] = $newArrayBuilder->getArray(); @@ -1235,6 +1330,7 @@ private function resolveType(Expr $node): Type return TypeCombinator::union(...$resultTypes); } + $arrayType = new ArrayType(new MixedType(), new MixedType()); if ($arrayType->isSuperTypeOf($leftType)->yes() && $arrayType->isSuperTypeOf($rightType)->yes()) { @@ -1251,10 +1347,16 @@ private function resolveType(Expr $node): Type } $keyType = TypeCombinator::union(...$keyTypes); } - return new ArrayType( + + $arrayType = new ArrayType( $keyType, - TypeCombinator::union($leftType->getIterableValueType(), $rightType->getIterableValueType()) + TypeCombinator::union($leftType->getIterableValueType(), $rightType->getIterableValueType()), ); + + if ($leftType->isIterableAtLeastOnce()->yes() || $rightType->isIterableAtLeastOnce()->yes()) { + return TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); + } + return $arrayType; } if ($leftType instanceof MixedType && $rightType instanceof MixedType) { @@ -1266,6 +1368,64 @@ private function resolveType(Expr $node): Type } } + if (($leftType instanceof IntegerRangeType || $leftType instanceof ConstantIntegerType || $leftType instanceof UnionType) && + ($rightType instanceof IntegerRangeType || $rightType instanceof ConstantIntegerType || $rightType instanceof UnionType) && + !($node instanceof Node\Expr\BinaryOp\Pow || $node instanceof Node\Expr\AssignOp\Pow)) { + + if ($leftType instanceof ConstantIntegerType) { + return $this->integerRangeMath( + $leftType, + $node, + $rightType, + ); + } elseif ($leftType instanceof UnionType) { + + $unionParts = []; + + foreach ($leftType->getTypes() as $type) { + if ($type instanceof IntegerRangeType || $type instanceof ConstantIntegerType) { + $unionParts[] = $this->integerRangeMath($type, $node, $rightType); + } else { + $unionParts[] = $type; + } + } + + $union = TypeCombinator::union(...$unionParts); + if ($leftType instanceof BenevolentUnionType) { + return TypeUtils::toBenevolentUnion($union)->toNumber(); + } + + return $union->toNumber(); + } + + return $this->integerRangeMath($leftType, $node, $rightType); + } + + $operatorSigil = null; + + if ($node instanceof BinaryOp) { + $operatorSigil = $node->getOperatorSigil(); + } + + if ($operatorSigil === null) { + $operatorSigil = self::OPERATOR_SIGIL_MAP[get_class($node)] ?? null; + } + + if ($operatorSigil !== null) { + $operatorTypeSpecifyingExtensions = $this->operatorTypeSpecifyingExtensionRegistry->getOperatorTypeSpecifyingExtensions($operatorSigil, $leftType, $rightType); + + /** @var Type[] $extensionTypes */ + $extensionTypes = []; + + foreach ($operatorTypeSpecifyingExtensions as $extension) { + $extensionTypes[] = $extension->specifyType($operatorSigil, $leftType, $rightType); + } + + if (count($extensionTypes) > 0) { + return TypeCombinator::union(...$extensionTypes); + } + } + $types = TypeCombinator::union($leftType, $rightType); if ( $leftType instanceof ArrayType @@ -1277,6 +1437,9 @@ private function resolveType(Expr $node): Type $leftNumberType = $leftType->toNumber(); $rightNumberType = $rightType->toNumber(); + if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) { + return new ErrorType(); + } if ( (new FloatType())->isSuperTypeOf($leftNumberType)->yes() @@ -1316,25 +1479,117 @@ private function resolveType(Expr $node): Type } elseif ($node instanceof String_) { return new ConstantStringType($node->value); } elseif ($node instanceof Node\Scalar\Encapsed) { - $constantString = new ConstantStringType(''); + $parts = []; foreach ($node->parts as $part) { if ($part instanceof EncapsedStringPart) { - $partStringType = new ConstantStringType($part->value); - } else { - $partStringType = $this->getType($part)->toString(); - if ($partStringType instanceof ErrorType) { - return new ErrorType(); + $parts[] = new ConstantStringType($part->value); + continue; + } + + $partStringType = $this->getType($part)->toString(); + if ($partStringType instanceof ErrorType) { + return new ErrorType(); + } + + $parts[] = $partStringType; + } + + $constantString = new ConstantStringType(''); + foreach ($parts as $part) { + if ($part instanceof ConstantStringType) { + $constantString = $constantString->append($part); + continue; + } + + $isNonEmpty = false; + $isLiteralString = true; + foreach ($parts as $partType) { + if ($partType->isNonEmptyString()->yes()) { + $isNonEmpty = true; } - if (!$partStringType instanceof ConstantStringType) { - return new StringType(); + if ($partType->isLiteralString()->yes()) { + continue; } + $isLiteralString = false; + } + + $accessoryTypes = []; + if ($isNonEmpty === true) { + $accessoryTypes[] = new AccessoryNonEmptyStringType(); + } + if ($isLiteralString === true) { + $accessoryTypes[] = new AccessoryLiteralStringType(); + } + if (count($accessoryTypes) > 0) { + $accessoryTypes[] = new StringType(); + return new IntersectionType($accessoryTypes); } - $constantString = $constantString->append($partStringType); + return new StringType(); } + return $constantString; } elseif ($node instanceof DNumber) { return new ConstantFloatType($node->value); + } elseif ($node instanceof Expr\CallLike && $node->isFirstClassCallable()) { + if ($node instanceof FuncCall) { + if ($node->name instanceof Name) { + if ($this->reflectionProvider->hasFunction($node->name, $this)) { + return $this->createFirstClassCallable( + $this->reflectionProvider->getFunction($node->name, $this)->getVariants(), + ); + } + + return new ObjectType(Closure::class); + } + + $callableType = $this->getType($node->name); + if (!$callableType->isCallable()->yes()) { + return new ObjectType(Closure::class); + } + + return $this->createFirstClassCallable( + $callableType->getCallableParametersAcceptors($this), + ); + } + + if ($node instanceof MethodCall) { + if (!$node->name instanceof Node\Identifier) { + return new ObjectType(Closure::class); + } + + $varType = $this->getType($node->var); + $method = $this->getMethodReflection($varType, $node->name->toString()); + if ($method === null) { + return new ObjectType(Closure::class); + } + + return $this->createFirstClassCallable($method->getVariants()); + } + + if ($node instanceof Expr\StaticCall) { + if (!$node->class instanceof Name) { + return new ObjectType(Closure::class); + } + + $classType = $this->resolveTypeByName($node->class); + if (!$node->name instanceof Node\Identifier) { + return new ObjectType(Closure::class); + } + + $methodName = $node->name->toString(); + if (!$classType->hasMethod($methodName)->yes()) { + return new ObjectType(Closure::class); + } + + return $this->createFirstClassCallable($classType->getMethod($methodName, $this)->getVariants()); + } + + if ($node instanceof New_) { + return new ErrorType(); + } + + throw new ShouldNotHappenException(); } elseif ($node instanceof Expr\Closure || $node instanceof Expr\ArrowFunction) { $parameters = []; $isVariadic = false; @@ -1356,17 +1611,17 @@ private function resolveType(Expr $node): Type $isVariadic = true; } if (!$param->var instanceof Variable || !is_string($param->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $parameters[] = new NativeParameterReflection( $param->var->name, $firstOptionalParameterIndex !== null && $i >= $firstOptionalParameterIndex, - $this->getFunctionType($param->type, $param->type === null, false), + $this->getFunctionType($param->type, $this->isParameterValueNullable($param), false), $param->byRef ? PassedByReference::createCreatesNewVariable() : PassedByReference::createNo(), $param->variadic, - $param->default !== null ? $this->getType($param->default) : null + $param->default !== null ? $this->getType($param->default) : null, ); } @@ -1380,11 +1635,22 @@ private function resolveType(Expr $node): Type if ( $functionName === 'array_map' && $argOrder === 0 - && isset($funcCall->args[1]) + && isset($funcCall->getArgs()[1]) ) { - $callableParameters = [ - new DummyParameter('item', $this->getType($funcCall->args[1]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null), - ]; + if (!isset($funcCall->getArgs()[2])) { + $callableParameters = [ + new DummyParameter('item', $this->getType($funcCall->getArgs()[1]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null), + ]; + } else { + $callableParameters = []; + foreach ($funcCall->getArgs() as $i => $funcCallArg) { + if ($i === 0) { + continue; + } + + $callableParameters[] = new DummyParameter('item', $this->getType($funcCallArg->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null); + } + } } } } @@ -1486,7 +1752,7 @@ private function resolveType(Expr $node): Type $valueTypes[] = $yieldFromType->getIterableValueType(); } - $returnType = new GenericObjectType(\Generator::class, [ + $returnType = new GenericObjectType(Generator::class, [ TypeCombinator::union(...$keyTypes), TypeCombinator::union(...$valueTypes), new MixedType(), @@ -1500,7 +1766,7 @@ private function resolveType(Expr $node): Type return new ClosureType( $parameters, $returnType, - $isVariadic + $isVariadic, ); } elseif ($node instanceof New_) { if ($node->class instanceof Name) { @@ -1534,7 +1800,7 @@ private function resolveType(Expr $node): Type } elseif ($node instanceof Array_) { $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); - if (count($node->items) > 256) { + if (count($node->items) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { $arrayBuilder->degradeToGeneralArray(); } foreach ($node->items as $arrayItem) { @@ -1545,17 +1811,34 @@ private function resolveType(Expr $node): Type $valueType = $this->getType($arrayItem->value); if ($arrayItem->unpack) { if ($valueType instanceof ConstantArrayType) { - foreach ($valueType->getValueTypes() as $innerValueType) { - $arrayBuilder->setOffsetValueType(null, $innerValueType); + $hasStringKey = false; + foreach ($valueType->getKeyTypes() as $keyType) { + if ($keyType instanceof ConstantStringType) { + $hasStringKey = true; + break; + } + } + + foreach ($valueType->getValueTypes() as $i => $innerValueType) { + if ($hasStringKey && $this->phpVersion->supportsArrayUnpackingWithStringKeys()) { + $arrayBuilder->setOffsetValueType($valueType->getKeyTypes()[$i], $innerValueType); + } else { + $arrayBuilder->setOffsetValueType(null, $innerValueType); + } } } else { $arrayBuilder->degradeToGeneralArray(); - $arrayBuilder->setOffsetValueType(new IntegerType(), $valueType->getIterableValueType()); + + if (! (new StringType())->isSuperTypeOf($valueType->getIterableKeyType())->no() && $this->phpVersion->supportsArrayUnpackingWithStringKeys()) { + $arrayBuilder->setOffsetValueType($valueType->getIterableKeyType(), $valueType->getIterableValueType()); + } else { + $arrayBuilder->setOffsetValueType(new IntegerType(), $valueType->getIterableValueType(), !$valueType->isIterableAtLeastOnce()->yes() && !$valueType->getIterableValueType()->isIterableAtLeastOnce()->yes()); + } } } else { $arrayBuilder->setOffsetValueType( $arrayItem->key !== null ? $this->getType($arrayItem->key) : null, - $valueType + $valueType, ); } } @@ -1567,9 +1850,9 @@ private function resolveType(Expr $node): Type return $this->getType($node->expr)->toBoolean(); } elseif ($node instanceof Double) { return $this->getType($node->expr)->toFloat(); - } elseif ($node instanceof \PhpParser\Node\Expr\Cast\String_) { + } elseif ($node instanceof Node\Expr\Cast\String_) { return $this->getType($node->expr)->toString(); - } elseif ($node instanceof \PhpParser\Node\Expr\Cast\Array_) { + } elseif ($node instanceof Node\Expr\Cast\Array_) { return $this->getType($node->expr)->toArray(); } elseif ($node instanceof Node\Scalar\MagicConst\Line) { return new ConstantIntegerType($node->getLine()); @@ -1596,7 +1879,7 @@ private function resolveType(Expr $node): Type } if ($function instanceof MethodReflection) { return new ConstantStringType( - sprintf('%s::%s', $function->getDeclaringClass()->getName(), $function->getName()) + sprintf('%s::%s', $function->getDeclaringClass()->getName(), $function->getName()), ); } @@ -1638,6 +1921,7 @@ private function resolveType(Expr $node): Type } elseif ($node instanceof Expr\PreInc || $node instanceof Expr\PreDec) { $varType = $this->getType($node->var); $varScalars = TypeUtils::getConstantScalars($varType); + $stringType = new StringType(); if (count($varScalars) > 0) { $newTypes = []; @@ -1652,16 +1936,18 @@ private function resolveType(Expr $node): Type $newTypes[] = $this->getTypeFromValue($varValue); } return TypeCombinator::union(...$newTypes); - } elseif ($varType instanceof IntegerRangeType) { - return $varType->shift($node instanceof Expr\PreInc ? +1 : -1); + } elseif ($stringType->isSuperTypeOf($varType)->yes()) { + if ($varType->isLiteralString()->yes()) { + return new IntersectionType([$stringType, new AccessoryLiteralStringType()]); + } + return $stringType; } - $stringType = new StringType(); - if ($stringType->isSuperTypeOf($varType)->yes()) { - return $stringType; + if ($node instanceof Expr\PreInc) { + return $this->getType(new BinaryOp\Plus($node->var, new LNumber(1))); } - return $varType->toNumber(); + return $this->getType(new BinaryOp\Minus($node->var, new LNumber(1))); } elseif ($node instanceof Expr\Yield_) { $functionReflection = $this->getFunction(); if ($functionReflection === null) { @@ -1673,7 +1959,7 @@ private function resolveType(Expr $node): Type return new MixedType(); } - $generatorSendType = GenericTypeVariableResolver::getType($returnType, \Generator::class, 'TSend'); + $generatorSendType = GenericTypeVariableResolver::getType($returnType, Generator::class, 'TSend'); if ($generatorSendType === null) { return new MixedType(); } @@ -1686,7 +1972,7 @@ private function resolveType(Expr $node): Type return new MixedType(); } - $generatorReturnType = GenericTypeVariableResolver::getType($yieldFromType, \Generator::class, 'TReturn'); + $generatorReturnType = GenericTypeVariableResolver::getType($yieldFromType, Generator::class, 'TReturn'); if ($generatorReturnType === null) { return new MixedType(); } @@ -1704,7 +1990,7 @@ private function resolveType(Expr $node): Type } if (count($arm->conds) === 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $filteringExpr = null; @@ -1737,62 +2023,41 @@ private function resolveType(Expr $node): Type } if ($node instanceof Expr\BinaryOp\Coalesce) { - if ($node->left instanceof Expr\ArrayDimFetch && $node->left->dim !== null) { - $dimType = $this->getType($node->left->dim); - $varType = $this->getType($node->left->var); - $hasOffset = $varType->hasOffsetValueType($dimType); - $leftType = $this->getType($node->left); - $rightType = $this->filterByFalseyValue( - new BinaryOp\NotIdentical($node->left, new ConstFetch(new Name('null'))) - )->getType($node->right); - if ($hasOffset->no()) { - return $rightType; - } elseif ($hasOffset->yes()) { - $offsetValueType = $varType->getOffsetValueType($dimType); - if ($offsetValueType->isSuperTypeOf(new NullType())->no()) { - return TypeCombinator::removeNull($leftType); - } - } - - return TypeCombinator::union( - TypeCombinator::removeNull($leftType), - $rightType - ); - } - $leftType = $this->getType($node->left); $rightType = $this->filterByFalseyValue( - new BinaryOp\NotIdentical($node->left, new ConstFetch(new Name('null'))) + new BinaryOp\NotIdentical($node->left, new ConstFetch(new Name('null'))), )->getType($node->right); - if ($leftType instanceof ErrorType || $leftType instanceof NullType) { - return $rightType; - } - if ( - TypeCombinator::containsNull($leftType) - || $node->left instanceof PropertyFetch - || ( - $node->left instanceof Variable - && is_string($node->left->name) - && !$this->hasVariableType($node->left->name)->yes() - ) - ) { + $result = $this->issetCheck($node->left, static function (Type $type): ?bool { + $isNull = (new NullType())->isSuperTypeOf($type); + if ($isNull->maybe()) { + return null; + } + + return !$isNull->yes(); + }); + + if ($result === null) { return TypeCombinator::union( TypeCombinator::removeNull($leftType), - $rightType + $rightType, ); } - return TypeCombinator::removeNull($leftType); + if ($result) { + return TypeCombinator::removeNull($leftType); + } + + return $rightType; } if ($node instanceof ConstFetch) { $constName = (string) $node->name; $loweredConstName = strtolower($constName); if ($loweredConstName === 'true') { - return new \PHPStan\Type\Constant\ConstantBooleanType(true); + return new ConstantBooleanType(true); } elseif ($loweredConstName === 'false') { - return new \PHPStan\Type\Constant\ConstantBooleanType(false); + return new ConstantBooleanType(false); } elseif ($loweredConstName === 'null') { return new NullType(); } @@ -1801,23 +2066,199 @@ private function resolveType(Expr $node): Type if (array_key_exists($node->name->toCodeString(), $this->constantTypes)) { return $this->resolveConstantType($node->name->toString(), $this->constantTypes[$node->name->toCodeString()]); } - } - - if ($this->getNamespace() !== null) { - $constantName = new FullyQualified([$this->getNamespace(), $constName]); - if (array_key_exists($constantName->toCodeString(), $this->constantTypes)) { - return $this->resolveConstantType($constantName->toString(), $this->constantTypes[$constantName->toCodeString()]); + } + + if ($this->getNamespace() !== null) { + $constantName = new FullyQualified([$this->getNamespace(), $constName]); + if (array_key_exists($constantName->toCodeString(), $this->constantTypes)) { + return $this->resolveConstantType($constantName->toString(), $this->constantTypes[$constantName->toCodeString()]); + } + } + + $constantName = new FullyQualified($constName); + if (array_key_exists($constantName->toCodeString(), $this->constantTypes)) { + return $this->resolveConstantType($constantName->toString(), $this->constantTypes[$constantName->toCodeString()]); + } + + if ($this->reflectionProvider->hasConstant($node->name, $this)) { + /** @var string $resolvedConstantName */ + $resolvedConstantName = $this->reflectionProvider->resolveConstantName($node->name, $this); + // core, https://www.php.net/manual/en/reserved.constants.php + if ($resolvedConstantName === 'PHP_VERSION') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_MAJOR_VERSION') { + return IntegerRangeType::fromInterval(5, null); + } + if ($resolvedConstantName === 'PHP_MINOR_VERSION') { + return IntegerRangeType::fromInterval(0, null); + } + if ($resolvedConstantName === 'PHP_RELEASE_VERSION') { + return IntegerRangeType::fromInterval(0, null); + } + if ($resolvedConstantName === 'PHP_VERSION_ID') { + return IntegerRangeType::fromInterval(50207, null); + } + if ($resolvedConstantName === 'PHP_ZTS') { + return new UnionType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ]); + } + if ($resolvedConstantName === 'PHP_DEBUG') { + return new UnionType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ]); + } + if ($resolvedConstantName === 'PHP_MAXPATHLEN') { + return IntegerRangeType::fromInterval(1, null); + } + if ($resolvedConstantName === 'PHP_OS') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_OS_FAMILY') { + return new UnionType([ + new ConstantStringType('Windows'), + new ConstantStringType('BSD'), + new ConstantStringType('Darwin'), + new ConstantStringType('Solaris'), + new ConstantStringType('Linux'), + new ConstantStringType('Unknown'), + ]); + } + if ($resolvedConstantName === 'PHP_SAPI') { + return new UnionType([ + new ConstantStringType('apache'), + new ConstantStringType('apache2handler'), + new ConstantStringType('cgi'), + new ConstantStringType('cli'), + new ConstantStringType('cli-server'), + new ConstantStringType('embed'), + new ConstantStringType('fpm-fcgi'), + new ConstantStringType('litespeed'), + new ConstantStringType('phpdbg'), + new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]), + ]); + } + if ($resolvedConstantName === 'PHP_EOL') { + return new UnionType([ + new ConstantStringType("\n"), + new ConstantStringType("\r\n"), + ]); + } + if ($resolvedConstantName === 'PHP_INT_MAX') { + return PHP_INT_SIZE === 8 + ? new UnionType([new ConstantIntegerType(2147483647), new ConstantIntegerType(9223372036854775807)]) + : new ConstantIntegerType(2147483647); + } + if ($resolvedConstantName === 'PHP_INT_MIN') { + // Why the -1 you might wonder, the answer is to fit it into an int :/ see https://3v4l.org/4SHIQ + return PHP_INT_SIZE === 8 + ? new UnionType([new ConstantIntegerType(-9223372036854775807 - 1), new ConstantIntegerType(-2147483647 - 1)]) + : new ConstantIntegerType(-2147483647 - 1); + } + if ($resolvedConstantName === 'PHP_INT_SIZE') { + return new UnionType([ + new ConstantIntegerType(4), + new ConstantIntegerType(8), + ]); + } + if ($resolvedConstantName === 'PHP_FLOAT_DIG') { + return IntegerRangeType::fromInterval(1, null); + } + if ($resolvedConstantName === 'PHP_EXTENSION_DIR') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); } - } - - $constantName = new FullyQualified($constName); - if (array_key_exists($constantName->toCodeString(), $this->constantTypes)) { - return $this->resolveConstantType($constantName->toString(), $this->constantTypes[$constantName->toCodeString()]); - } - - if ($this->reflectionProvider->hasConstant($node->name, $this)) { - /** @var string $resolvedConstantName */ - $resolvedConstantName = $this->reflectionProvider->resolveConstantName($node->name, $this); + if ($resolvedConstantName === 'PHP_PREFIX') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_BINDIR') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_BINARY') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_MANDIR') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_LIBDIR') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_DATADIR') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_SYSCONFDIR') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_LOCALSTATEDIR') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_CONFIG_FILE_PATH') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + if ($resolvedConstantName === 'PHP_SHLIB_SUFFIX') { + return new UnionType([ + new ConstantStringType('so'), + new ConstantStringType('dll'), + ]); + } + if ($resolvedConstantName === 'PHP_FD_SETSIZE') { + return IntegerRangeType::fromInterval(1, null); + } + if ($resolvedConstantName === '__COMPILER_HALT_OFFSET__') { + return IntegerRangeType::fromInterval(0, null); + } + // core other, https://www.php.net/manual/en/info.constants.php + if ($resolvedConstantName === 'PHP_WINDOWS_VERSION_MAJOR') { + return IntegerRangeType::fromInterval(4, null); + } + if ($resolvedConstantName === 'PHP_WINDOWS_VERSION_MINOR') { + return IntegerRangeType::fromInterval(0, null); + } + if ($resolvedConstantName === 'PHP_WINDOWS_VERSION_BUILD') { + return IntegerRangeType::fromInterval(1, null); + } + // dir, https://www.php.net/manual/en/dir.constants.php if ($resolvedConstantName === 'DIRECTORY_SEPARATOR') { return new UnionType([ new ConstantStringType('/'), @@ -1830,14 +2271,26 @@ private function resolveType(Expr $node): Type new ConstantStringType(';'), ]); } - if ($resolvedConstantName === 'PHP_EOL') { - return new UnionType([ - new ConstantStringType("\n"), - new ConstantStringType("\r\n"), + // iconv, https://www.php.net/manual/en/iconv.constants.php + if ($resolvedConstantName === 'ICONV_IMPL') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), ]); } - if ($resolvedConstantName === '__COMPILER_HALT_OFFSET__') { - return new IntegerType(); + // libxml, https://www.php.net/manual/en/libxml.constants.php + if ($resolvedConstantName === 'LIBXML_VERSION') { + return IntegerRangeType::fromInterval(1, null); + } + if ($resolvedConstantName === 'LIBXML_DOTTED_VERSION') { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + // openssl, https://www.php.net/manual/en/openssl.constants.php + if ($resolvedConstantName === 'OPENSSL_VERSION_NUMBER') { + return IntegerRangeType::fromInterval(1, null); } $constantType = $this->reflectionProvider->getConstant($node->name, $this)->getValueType(); @@ -1848,6 +2301,7 @@ private function resolveType(Expr $node): Type return new ErrorType(); } elseif ($node instanceof Node\Expr\ClassConstFetch && $node->name instanceof Node\Identifier) { $constantName = $node->name->name; + $isObject = false; if ($node->class instanceof Name) { $constantClass = (string) $node->class; $constantClassType = new ObjectType($constantClass); @@ -1862,7 +2316,9 @@ private function resolveType(Expr $node): Type if (strtolower($constantName) === 'class') { return new GenericClassStringType(new StaticType($this->getClassReflection())); } - return new MixedType(); + + $namesToResolve[] = 'static'; + $isObject = true; } } if (in_array(strtolower($constantClass), $namesToResolve, true)) { @@ -1878,11 +2334,15 @@ private function resolveType(Expr $node): Type } } else { $constantClassType = $this->getType($node->class); + $isObject = true; } $referencedClasses = TypeUtils::getDirectClassNames($constantClassType); if (strtolower($constantName) === 'class') { if (count($referencedClasses) === 0) { + if ((new ObjectWithoutClassType())->isSuperTypeOf($constantClassType)->yes()) { + return new ClassStringType(); + } return new ErrorType(); } $classTypes = []; @@ -1898,17 +2358,43 @@ private function resolveType(Expr $node): Type continue; } - $propertyClassReflection = $this->reflectionProvider->getClass($referencedClass); - if (!$propertyClassReflection->hasConstant($constantName)) { + $constantClassReflection = $this->reflectionProvider->getClass($referencedClass); + if (!$constantClassReflection->hasConstant($constantName)) { continue; } - $constantType = $propertyClassReflection->getConstant($constantName)->getValueType(); + if ($constantClassReflection->isEnum() && $constantClassReflection->hasEnumCase($constantName)) { + $types[] = new EnumCaseObjectType($constantClassReflection->getName(), $constantName); + continue; + } + + $constantReflection = $constantClassReflection->getConstant($constantName); + if ( + $constantReflection instanceof ClassConstantReflection + && $isObject + && !$constantClassReflection->isFinal() + && !$constantReflection->hasPhpDocType() + ) { + return new MixedType(); + } + + if ( + $isObject + && ( + !$constantReflection instanceof ClassConstantReflection + || !$constantClassReflection->isFinal() + ) + ) { + $constantType = $constantReflection->getValueType(); + } else { + $constantType = ConstantTypeHelper::getTypeFromValue($constantReflection->getValue()); + } + if ( $constantType instanceof ConstantType - && in_array(sprintf('%s::%s', $propertyClassReflection->getName(), $constantName), $this->dynamicConstantNames, true) + && in_array(sprintf('%s::%s', $constantClassReflection->getName(), $constantName), $this->dynamicConstantNames, true) ) { - $constantType = $constantType->generalize(); + $constantType = $constantType->generalize(GeneralizePrecision::lessSpecific()); } $types[] = $constantType; } @@ -1937,7 +2423,7 @@ private function resolveType(Expr $node): Type } return TypeCombinator::union( TypeCombinator::remove($this->filterByTruthyValue($node->cond)->getType($node->cond), StaticTypeFactory::falsey()), - $this->filterByFalseyValue($node->cond)->getType($node->else) + $this->filterByFalseyValue($node->cond)->getType($node->else), ); } @@ -1952,7 +2438,7 @@ private function resolveType(Expr $node): Type return TypeCombinator::union( $this->filterByTruthyValue($node->cond)->getType($node->if), - $this->filterByFalseyValue($node->cond)->getType($node->else) + $this->filterByFalseyValue($node->cond)->getType($node->else), ); } @@ -1970,8 +2456,8 @@ private function resolveType(Expr $node): Type $this->getTypeFromArrayDimFetch( $node, $this->getType($node->dim), - $this->getType($node->var) - ) + $this->getType($node->var), + ), ); } @@ -1980,7 +2466,7 @@ private function resolveType(Expr $node): Type $returnType = $this->methodCallReturnType( $this->getType($node->var), $node->name->name, - $node + $node, ); if ($returnType === null) { return new ErrorType(); @@ -2000,7 +2486,7 @@ private function resolveType(Expr $node): Type return TypeCombinator::union( $this->filterByTruthyValue(new BinaryOp\NotIdentical($node->var, new ConstFetch(new Name('null')))) ->getType(new MethodCall($node->var, $node->name, $node->args)), - new NullType() + new NullType(), ); } @@ -2009,16 +2495,27 @@ private function resolveType(Expr $node): Type if ($node->class instanceof Name) { $staticMethodCalledOnType = $this->resolveTypeByName($node->class); } else { - $staticMethodCalledOnType = $this->getType($node->class); - if ($staticMethodCalledOnType instanceof GenericClassStringType) { - $staticMethodCalledOnType = $staticMethodCalledOnType->getGenericType(); - } + $staticMethodCalledOnType = TypeTraverser::map($this->getType($node->class), static function (Type $type, callable $traverse): Type { + if ($type instanceof UnionType) { + return $traverse($type); + } + + if ($type instanceof GenericClassStringType) { + return $type->getGenericType(); + } + + if ($type instanceof ConstantStringType && $type->isClassString()) { + return new ObjectType($type->getValue()); + } + + return $type; + }); } $returnType = $this->methodCallReturnType( $staticMethodCalledOnType, $node->name->toString(), - $node + $node, ); if ($returnType === null) { return new ErrorType(); @@ -2039,7 +2536,7 @@ private function resolveType(Expr $node): Type $returnType = $this->propertyFetchType( $this->getType($node->var), $node->name->name, - $node + $node, ); if ($returnType === null) { return new ErrorType(); @@ -2059,7 +2556,7 @@ private function resolveType(Expr $node): Type return TypeCombinator::union( $this->filterByTruthyValue(new BinaryOp\NotIdentical($node->var, new ConstFetch(new Name('null')))) ->getType(new PropertyFetch($node->var, $node->name)), - new NullType() + new NullType(), ); } @@ -2080,7 +2577,7 @@ private function resolveType(Expr $node): Type $returnType = $this->propertyFetchType( $staticPropertyFetchedOnType, $node->name->toString(), - $node + $node, ); if ($returnType === null) { return new ErrorType(); @@ -2105,8 +2602,8 @@ private function resolveType(Expr $node): Type return ParametersAcceptorSelector::selectFromArgs( $this, - $node->args, - $calledOnType->getCallableParametersAcceptors($this) + $node->getArgs(), + $calledOnType->getCallableParametersAcceptors($this), )->getReturnType(); } @@ -2120,19 +2617,106 @@ private function resolveType(Expr $node): Type continue; } - return $dynamicFunctionReturnTypeExtension->getTypeFromFunctionCall($functionReflection, $node, $this); + $resolvedType = $dynamicFunctionReturnTypeExtension->getTypeFromFunctionCall( + $functionReflection, + $node, + $this, + ); + if ($resolvedType !== null) { + return $resolvedType; + } } return ParametersAcceptorSelector::selectFromArgs( $this, - $node->args, - $functionReflection->getVariants() + $node->getArgs(), + $functionReflection->getVariants(), )->getReturnType(); } return new MixedType(); } + private function resolveConcatType(Expr\BinaryOp\Concat|Expr\AssignOp\Concat $node): Type + { + if ($node instanceof Node\Expr\AssignOp) { + $left = $node->var; + $right = $node->expr; + } else { + $left = $node->left; + $right = $node->right; + } + + $leftStringType = $this->getType($left)->toString(); + $rightStringType = $this->getType($right)->toString(); + if (TypeCombinator::union( + $leftStringType, + $rightStringType, + ) instanceof ErrorType) { + return new ErrorType(); + } + + if ($leftStringType instanceof ConstantStringType && $leftStringType->getValue() === '') { + return $rightStringType; + } + + if ($rightStringType instanceof ConstantStringType && $rightStringType->getValue() === '') { + return $leftStringType; + } + + if ($leftStringType instanceof ConstantStringType && $rightStringType instanceof ConstantStringType) { + return $leftStringType->append($rightStringType); + } + + // we limit the number of union-types for performance reasons + if ($leftStringType instanceof UnionType && count($leftStringType->getTypes()) <= 16 && $rightStringType instanceof ConstantStringType) { + $constantStrings = TypeUtils::getConstantStrings($leftStringType); + if (count($constantStrings) > 0) { + $strings = []; + foreach ($constantStrings as $constantString) { + if ($constantString->getValue() === '') { + $strings[] = $rightStringType; + + continue; + } + $strings[] = $constantString->append($rightStringType); + } + return TypeCombinator::union(...$strings); + } + } + if ($rightStringType instanceof UnionType && count($rightStringType->getTypes()) <= 16 && $leftStringType instanceof ConstantStringType) { + $constantStrings = TypeUtils::getConstantStrings($rightStringType); + if (count($constantStrings) > 0) { + $strings = []; + foreach ($constantStrings as $constantString) { + if ($constantString->getValue() === '') { + $strings[] = $leftStringType; + + continue; + } + $strings[] = $leftStringType->append($constantString); + } + return TypeCombinator::union(...$strings); + } + } + + $accessoryTypes = []; + if ($leftStringType->isNonEmptyString()->or($rightStringType->isNonEmptyString())->yes()) { + $accessoryTypes[] = new AccessoryNonEmptyStringType(); + } + + if ($leftStringType->isLiteralString()->and($rightStringType->isLiteralString())->yes()) { + $accessoryTypes[] = new AccessoryLiteralStringType(); + } + + if (count($accessoryTypes) > 0) { + $accessoryTypes[] = new StringType(); + return new IntersectionType($accessoryTypes); + } + + return new StringType(); + } + private function getNullsafeShortCircuitingType(Expr $expr, Type $type): Type { if ($expr instanceof Expr\NullsafePropertyFetch || $expr instanceof Expr\NullsafeMethodCall) { @@ -2167,10 +2751,201 @@ private function getNullsafeShortCircuitingType(Expr $expr, Type $type): Type return $type; } + /** + * @param callable(Type): ?bool $typeCallback + */ + private function issetCheck(Expr $expr, callable $typeCallback, ?bool $result = null): ?bool + { + // mirrored in PHPStan\Rules\IssetCheck + if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) { + $hasVariable = $this->hasVariableType($expr->name); + if ($hasVariable->maybe()) { + return null; + } + + if ($result === null) { + if ($hasVariable->yes()) { + if ($expr->name === '_SESSION') { + return null; + } + + return $typeCallback($this->getVariableType($expr->name)); + } + + return false; + } + + return $result; + } elseif ($expr instanceof Node\Expr\ArrayDimFetch && $expr->dim !== null) { + $type = $this->treatPhpDocTypesAsCertain + ? $this->getType($expr->var) + : $this->getNativeType($expr->var); + $dimType = $this->treatPhpDocTypesAsCertain + ? $this->getType($expr->dim) + : $this->getNativeType($expr->dim); + $hasOffsetValue = $type->hasOffsetValueType($dimType); + if (!$type->isOffsetAccessible()->yes()) { + return $result ?? $this->issetCheckUndefined($expr->var); + } + + if ($hasOffsetValue->no()) { + if ($result !== null) { + return $result; + } + + return false; + } + + if ($hasOffsetValue->maybe()) { + return null; + } + + // If offset is 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()) { + if ($result !== null) { + return $result; + } + + $result = $typeCallback($type->getOffsetValueType($dimType)); + + if ($result !== null) { + return $this->issetCheck($expr->var, $typeCallback, $result); + } + } + + // Has offset, it is nullable + return null; + + } elseif ($expr instanceof Node\Expr\PropertyFetch || $expr instanceof Node\Expr\StaticPropertyFetch) { + + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $this); + + if ($propertyReflection === null) { + if ($expr instanceof Node\Expr\PropertyFetch) { + return $this->issetCheckUndefined($expr->var); + } + + if ($expr->class instanceof Expr) { + return $this->issetCheckUndefined($expr->class); + } + + return null; + } + + if (!$propertyReflection->isNative()) { + if ($expr instanceof Node\Expr\PropertyFetch) { + return $this->issetCheckUndefined($expr->var); + } + + if ($expr->class instanceof Expr) { + return $this->issetCheckUndefined($expr->class); + } + + return null; + } + + $nativeType = $propertyReflection->getNativeType(); + if (!$nativeType instanceof MixedType) { + if (!$this->isSpecified($expr)) { + if ($expr instanceof Node\Expr\PropertyFetch) { + return $this->issetCheckUndefined($expr->var); + } + + if ($expr->class instanceof Expr) { + return $this->issetCheckUndefined($expr->class); + } + + return null; + } + } + + if ($result !== null) { + return $result; + } + + $result = $typeCallback($propertyReflection->getWritableType()); + if ($result !== null) { + if ($expr instanceof Node\Expr\PropertyFetch) { + return $this->issetCheck($expr->var, $typeCallback, $result); + } + + if ($expr->class instanceof Expr) { + return $this->issetCheck($expr->class, $typeCallback, $result); + } + } + + return $result; + } + + if ($result !== null) { + return $result; + } + + return $typeCallback($this->getType($expr)); + } + + private function issetCheckUndefined(Expr $expr): ?bool + { + if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) { + $hasVariable = $this->hasVariableType($expr->name); + if (!$hasVariable->no()) { + return null; + } + + return false; + } + + 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); + } + + if (!$hasOffsetValue->no()) { + return $this->issetCheckUndefined($expr->var); + } + + return false; + } + + if ($expr instanceof Expr\PropertyFetch) { + return $this->issetCheckUndefined($expr->var); + } + + if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) { + return $this->issetCheckUndefined($expr->class); + } + + return null; + } + + /** + * @param ParametersAcceptor[] $variants + */ + private function createFirstClassCallable(array $variants): Type + { + $closureTypes = []; + foreach ($variants as $variant) { + $parameters = $variant->getParameters(); + $closureTypes[] = new ClosureType( + $parameters, + $variant->getReturnType(), + $variant->isVariadic(), + $variant->getTemplateTypeMap(), + $variant->getResolvedTemplateTypeMap(), + ); + } + + return TypeCombinator::union(...$closureTypes); + } + private function resolveConstantType(string $constantName, Type $constantType): Type { if ($constantType instanceof ConstantType && in_array($constantName, $this->dynamicConstantNames, true)) { - return $constantType->generalize(); + return $constantType->generalize(GeneralizePrecision::lessSpecific()); } return $constantType; @@ -2191,8 +2966,8 @@ public function getNativeType(Expr $expr): Type $this->getTypeFromArrayDimFetch( $expr, $this->getNativeType($expr->dim), - $this->getNativeType($expr->var) - ) + $this->getNativeType($expr->var), + ), ); } @@ -2217,6 +2992,7 @@ public function doNotTreatPhpDocTypesAsCertain(): Scope $this->parser, $this->nodeScopeResolver, $this->context, + $this->phpVersion, $this->declareStrictTypes, $this->constantTypes, $this->function, @@ -2232,9 +3008,9 @@ public function doNotTreatPhpDocTypesAsCertain(): Scope $this->inFunctionCallsStack, $this->dynamicConstantNames, false, - $this->objectFromNewClass, $this->afterExtractCall, - $this->parentScope + $this->parentScope, + $this->explicitMixedInUnknownGenericNew, ); } @@ -2243,13 +3019,13 @@ private function promoteNativeTypes(): self $variableTypes = $this->variableTypes; foreach ($this->nativeExpressionTypes as $expressionType => $type) { if (substr($expressionType, 0, 1) !== '$') { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $variableName = substr($expressionType, 1); $has = $this->hasVariableType($variableName); if ($has->no()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $variableTypes[$variableName] = new VariableTypeHolder($type, $has); @@ -2268,13 +3044,12 @@ private function promoteNativeTypes(): self $this->anonymousFunctionReflection, $this->inFirstLevelStatement, $this->currentlyAssignedExpressions, - [] + [], ); } /** - * @param \PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\StaticPropertyFetch $propertyFetch - * @return bool + * @param Node\Expr\PropertyFetch|Node\Expr\StaticPropertyFetch $propertyFetch */ private function hasPropertyNativeType($propertyFetch): bool { @@ -2294,22 +3069,22 @@ private function hasPropertyNativeType($propertyFetch): bool protected function getTypeFromArrayDimFetch( Expr\ArrayDimFetch $arrayDimFetch, Type $offsetType, - Type $offsetAccessibleType + Type $offsetAccessibleType, ): Type { if ($arrayDimFetch->dim === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - if ((new ObjectType(\ArrayAccess::class))->isSuperTypeOf($offsetAccessibleType)->yes()) { + if ((new ObjectType(ArrayAccess::class))->isSuperTypeOf($offsetAccessibleType)->yes()) { return $this->getType( new MethodCall( $arrayDimFetch->var, new Node\Identifier('offsetGet'), [ new Node\Arg($arrayDimFetch->dim), - ] - ) + ], + ), ); } @@ -2351,7 +3126,7 @@ private function calculateFromScalars(Expr $node, ConstantScalarType $leftType, } if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } /** @var float|int $leftNumberValue */ @@ -2381,7 +3156,7 @@ private function calculateFromScalars(Expr $node, ConstantScalarType $leftType, } if ($node instanceof Node\Expr\BinaryOp\Mod || $node instanceof Node\Expr\AssignOp\Mod) { - return $this->getTypeFromValue($leftNumberValue % $rightNumberValue); + return $this->getTypeFromValue(((int) $leftNumberValue) % ((int) $rightNumberValue)); } if ($node instanceof Expr\BinaryOp\ShiftLeft || $node instanceof Expr\AssignOp\ShiftLeft) { @@ -2422,7 +3197,7 @@ private function resolveExactName(Name $name): ?string return null; } $currentClassReflection = $this->getClassReflection(); - if ($currentClassReflection->getParentClass() !== false) { + if ($currentClassReflection->getParentClass() !== null) { return $currentClassReflection->getParentClass()->getName(); } return null; @@ -2448,7 +3223,7 @@ public function resolveName(Name $name): string return $this->getClassReflection()->getName(); } elseif ($originalClass === 'parent') { $currentClassReflection = $this->getClassReflection(); - if ($currentClassReflection->getParentClass() !== false) { + if ($currentClassReflection->getParentClass() !== null) { return $currentClassReflection->getParentClass()->getName(); } } @@ -2462,7 +3237,9 @@ public function resolveTypeByName(Name $name): TypeWithClassName { if ($name->toLowerString() === 'static' && $this->isInClass()) { if ($this->inClosureBindScopeClass !== null && $this->inClosureBindScopeClass !== 'static') { - return new StaticType($this->inClosureBindScopeClass); + if ($this->reflectionProvider->hasClass($this->inClosureBindScopeClass)) { + return new StaticType($this->reflectionProvider->getClass($this->inClosureBindScopeClass)); + } } return new StaticType($this->getClassReflection()); @@ -2470,11 +3247,14 @@ public function resolveTypeByName(Name $name): TypeWithClassName $originalClass = $this->resolveName($name); if ($this->isInClass()) { - if ($this->inClosureBindScopeClass !== null && $this->inClosureBindScopeClass !== 'static') { - $thisType = new ThisType($this->inClosureBindScopeClass); - } else { - $thisType = new ThisType($this->getClassReflection()); + if ($this->inClosureBindScopeClass !== null && $this->inClosureBindScopeClass !== 'static' && $originalClass === $this->getClassReflection()->getName()) { + if ($this->reflectionProvider->hasClass($this->inClosureBindScopeClass)) { + return new ThisType($this->reflectionProvider->getClass($this->inClosureBindScopeClass)); + } + return new ObjectType($this->inClosureBindScopeClass); } + + $thisType = new ThisType($this->getClassReflection()); $ancestor = $thisType->getAncestorWithClassName($originalClass); if ($ancestor !== null) { return $ancestor; @@ -2504,7 +3284,6 @@ public function isSpecified(Expr $node): bool /** * @param MethodReflection|FunctionReflection $reflection - * @return self */ public function pushInFunctionCall($reflection): self { @@ -2527,7 +3306,7 @@ public function pushInFunctionCall($reflection): self $this->nativeExpressionTypes, $stack, $this->afterExtractCall, - $this->parentScope + $this->parentScope, ); } @@ -2552,7 +3331,7 @@ public function popInFunctionCall(): self $this->nativeExpressionTypes, $stack, $this->afterExtractCall, - $this->parentScope + $this->parentScope, ); } @@ -2579,6 +3358,16 @@ public function isInClassExists(string $className): bool return (new ConstantBooleanType(true))->isSuperTypeOf($this->getType($expr))->yes(); } + /** @api */ + public function isInFunctionExists(string $functionName): bool + { + $expr = new FuncCall(new FullyQualified('function_exists'), [ + new Arg(new String_(ltrim($functionName, '\\'))), + ]); + + return (new ConstantBooleanType(true))->isSuperTypeOf($this->getType($expr))->yes(); + } + /** @api */ public function enterClass(ClassReflection $classReflection): self { @@ -2590,7 +3379,7 @@ public function enterClass(ClassReflection $classReflection): self $this->getNamespace(), [ 'this' => VariableTypeHolder::createYes(new ThisType($classReflection)), - ] + ], ); } @@ -2606,23 +3395,13 @@ public function enterTrait(ClassReflection $traitReflection): self $this->moreSpecificTypes, [], $this->inClosureBindScopeClass, - $this->anonymousFunctionReflection + $this->anonymousFunctionReflection, ); } /** * @api - * @param Node\Stmt\ClassMethod $classMethod - * @param TemplateTypeMap $templateTypeMap * @param Type[] $phpDocParameterTypes - * @param Type|null $phpDocReturnType - * @param Type|null $throwType - * @param string|null $deprecatedDescription - * @param bool $isDeprecated - * @param bool $isInternal - * @param bool $isFinal - * @param bool|null $isPure - * @return self */ public function enterClassMethod( Node\Stmt\ClassMethod $classMethod, @@ -2634,33 +3413,32 @@ public function enterClassMethod( bool $isDeprecated, bool $isInternal, bool $isFinal, - ?bool $isPure = null + ?bool $isPure = null, ): self { if (!$this->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $this->enterFunctionLike( new PhpMethodFromParserNodeReflection( $this->getClassReflection(), $classMethod, + $this->getFile(), $templateTypeMap, $this->getRealParameterTypes($classMethod), - array_map(static function (Type $type): Type { - return TemplateTypeHelper::toArgument($type); - }, $phpDocParameterTypes), + array_map(static fn (Type $type): Type => TemplateTypeHelper::toArgument($type), $phpDocParameterTypes), $this->getRealParameterDefaultValues($classMethod), - $this->transformStaticType($this->getFunctionType($classMethod->returnType, $classMethod->returnType === null, false)), + $this->transformStaticType($this->getFunctionType($classMethod->returnType, false, false)), $phpDocReturnType !== null ? TemplateTypeHelper::toArgument($phpDocReturnType) : null, $throwType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, - $isPure + $isPure, ), - !$classMethod->isStatic() + !$classMethod->isStatic(), ); } @@ -2684,7 +3462,6 @@ private function transformStaticType(Type $type): Type } /** - * @param Node\FunctionLike $functionLike * @return Type[] */ private function getRealParameterTypes(Node\FunctionLike $functionLike): array @@ -2692,12 +3469,12 @@ private function getRealParameterTypes(Node\FunctionLike $functionLike): array $realParameterTypes = []; foreach ($functionLike->getParams() as $parameter) { if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $realParameterTypes[$parameter->var->name] = $this->getFunctionType( $parameter->type, $this->isParameterValueNullable($parameter), - false + false, ); } @@ -2705,7 +3482,6 @@ private function getRealParameterTypes(Node\FunctionLike $functionLike): array } /** - * @param Node\FunctionLike $functionLike * @return Type[] */ private function getRealParameterDefaultValues(Node\FunctionLike $functionLike): array @@ -2716,7 +3492,7 @@ private function getRealParameterDefaultValues(Node\FunctionLike $functionLike): continue; } if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $realParameterDefaultValues[$parameter->var->name] = $this->getType($parameter->default); } @@ -2726,17 +3502,7 @@ private function getRealParameterDefaultValues(Node\FunctionLike $functionLike): /** * @api - * @param Node\Stmt\Function_ $function - * @param TemplateTypeMap $templateTypeMap * @param Type[] $phpDocParameterTypes - * @param Type|null $phpDocReturnType - * @param Type|null $throwType - * @param string|null $deprecatedDescription - * @param bool $isDeprecated - * @param bool $isInternal - * @param bool $isFinal - * @param bool|null $isPure - * @return self */ public function enterFunction( Node\Stmt\Function_ $function, @@ -2748,17 +3514,16 @@ public function enterFunction( bool $isDeprecated, bool $isInternal, bool $isFinal, - ?bool $isPure = null + ?bool $isPure = null, ): self { return $this->enterFunctionLike( new PhpFunctionFromParserNodeReflection( $function, + $this->getFile(), $templateTypeMap, $this->getRealParameterTypes($function), - array_map(static function (Type $type): Type { - return TemplateTypeHelper::toArgument($type); - }, $phpDocParameterTypes), + array_map(static fn (Type $type): Type => TemplateTypeHelper::toArgument($type), $phpDocParameterTypes), $this->getRealParameterDefaultValues($function), $this->getFunctionType($function->returnType, $function->returnType === null, false), $phpDocReturnType !== null ? TemplateTypeHelper::toArgument($phpDocReturnType) : null, @@ -2767,15 +3532,15 @@ public function enterFunction( $isDeprecated, $isInternal, $isFinal, - $isPure + $isPure, ), - false + false, ); } private function enterFunctionLike( PhpFunctionFromParserNodeReflection $functionReflection, - bool $preserveThis + bool $preserveThis, ): self { $variableTypes = []; @@ -2783,10 +3548,23 @@ private function enterFunctionLike( foreach (ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getParameters() as $parameter) { $parameterType = $parameter->getType(); if ($parameter->isVariadic()) { - $parameterType = new ArrayType(new IntegerType(), $parameterType); + if ($this->phpVersion->supportsNamedArguments()) { + $parameterType = new ArrayType(new UnionType([new IntegerType(), new StringType()]), $parameterType); + } else { + $parameterType = new ArrayType(new IntegerType(), $parameterType); + } } $variableTypes[$parameter->getName()] = VariableTypeHolder::createYes($parameterType); - $nativeExpressionTypes[sprintf('$%s', $parameter->getName())] = $parameter->getNativeType(); + + $nativeParameterType = $parameter->getNativeType(); + if ($parameter->isVariadic()) { + if ($this->phpVersion->supportsNamedArguments()) { + $nativeParameterType = new ArrayType(new UnionType([new IntegerType(), new StringType()]), $nativeParameterType); + } else { + $nativeParameterType = new ArrayType(new IntegerType(), $nativeParameterType); + } + } + $nativeExpressionTypes[sprintf('$%s', $parameter->getName())] = $nativeParameterType; } if ($preserveThis && array_key_exists('this', $this->variableTypes)) { @@ -2806,7 +3584,7 @@ private function enterFunctionLike( null, true, [], - $nativeExpressionTypes + $nativeExpressionTypes, ); } @@ -2817,7 +3595,7 @@ public function enterNamespace(string $namespaceName): self $this->isDeclareStrictTypes(), $this->constantTypes, null, - $namespaceName + $namespaceName, ); } @@ -2845,7 +3623,7 @@ public function enterClosureBind(?Type $thisType, string $scopeClass): self $this->moreSpecificTypes, $this->conditionalExpressions, $scopeClass, - $this->anonymousFunctionReflection + $this->anonymousFunctionReflection, ); } @@ -2868,7 +3646,7 @@ public function restoreOriginalScopeAfterClosureBind(self $originalScope): self $this->moreSpecificTypes, $this->conditionalExpressions, $originalScope->inClosureBindScopeClass, - $this->anonymousFunctionReflection + $this->anonymousFunctionReflection, ); } @@ -2887,7 +3665,7 @@ public function enterClosureCall(Type $thisType): self $this->moreSpecificTypes, $this->conditionalExpressions, $thisType instanceof TypeWithClassName ? $thisType->getClassName() : null, - $this->anonymousFunctionReflection + $this->anonymousFunctionReflection, ); } @@ -2899,18 +3677,16 @@ public function isInClosureBind(): bool /** * @api - * @param \PhpParser\Node\Expr\Closure $closure - * @param \PHPStan\Reflection\ParameterReflection[]|null $callableParameters - * @return self + * @param ParameterReflection[]|null $callableParameters */ public function enterAnonymousFunction( Expr\Closure $closure, - ?array $callableParameters = null + ?array $callableParameters = null, ): self { $anonymousFunctionReflection = $this->getType($closure); if (!$anonymousFunctionReflection instanceof ClosureType) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $scope = $this->enterAnonymousFunctionWithoutReflection($closure, $callableParameters); @@ -2931,18 +3707,16 @@ public function enterAnonymousFunction( $scope->nativeExpressionTypes, [], false, - $this + $this, ); } /** - * @param \PhpParser\Node\Expr\Closure $closure - * @param \PHPStan\Reflection\ParameterReflection[]|null $callableParameters - * @return self + * @param ParameterReflection[]|null $callableParameters */ private function enterAnonymousFunctionWithoutReflection( Expr\Closure $closure, - ?array $callableParameters = null + ?array $callableParameters = null, ): self { $variableTypes = []; @@ -2965,10 +3739,10 @@ private function enterAnonymousFunctionWithoutReflection( } if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $variableTypes[$parameter->var->name] = VariableTypeHolder::createYes( - $parameterType + $parameterType, ); } @@ -2976,7 +3750,7 @@ private function enterAnonymousFunctionWithoutReflection( $moreSpecificTypes = []; foreach ($closure->uses as $use) { if (!is_string($use->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ($use->byRef) { continue; @@ -2990,7 +3764,7 @@ private function enterAnonymousFunctionWithoutReflection( } $variableTypes[$variableName] = VariableTypeHolder::createYes($variableType); foreach ($this->moreSpecificTypes as $exprString => $moreSpecificType) { - $matches = \Nette\Utils\Strings::matchAll((string) $exprString, '#^\$([a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*)#'); + $matches = Strings::matchAll((string) $exprString, '#^\$([a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*)#'); if ($matches === []) { continue; } @@ -3024,19 +3798,19 @@ private function enterAnonymousFunctionWithoutReflection( $nativeTypes, [], false, - $this + $this, ); } /** * @api - * @param \PHPStan\Reflection\ParameterReflection[]|null $callableParameters + * @param ParameterReflection[]|null $callableParameters */ public function enterArrowFunction(Expr\ArrowFunction $arrowFunction, ?array $callableParameters = null): self { $anonymousFunctionReflection = $this->getType($arrowFunction); if (!$anonymousFunctionReflection instanceof ClosureType) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $scope = $this->enterArrowFunctionWithoutReflection($arrowFunction, $callableParameters); @@ -3057,18 +3831,19 @@ public function enterArrowFunction(Expr\ArrowFunction $arrowFunction, ?array $ca [], [], $scope->afterExtractCall, - $scope->parentScope + $scope->parentScope, ); } /** - * @param \PHPStan\Reflection\ParameterReflection[]|null $callableParameters + * @param ParameterReflection[]|null $callableParameters */ private function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFunction, ?array $callableParameters): self { $variableTypes = $this->variableTypes; $mixed = new MixedType(); $parameterVariables = []; + $parameterVariableExpressions = []; foreach ($arrowFunction->params as $i => $parameter) { if ($parameter->type === null) { $parameterType = $mixed; @@ -3093,11 +3868,12 @@ private function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFu } if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $variableTypes[$parameter->var->name] = VariableTypeHolder::createYes($parameterType); $parameterVariables[] = $parameter->var->name; + $parameterVariableExpressions[] = $parameter->var; } if ($arrowFunction->static) { @@ -3159,7 +3935,7 @@ private function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFu } } - return $this->scopeFactory->create( + $scope = $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), $this->constantTypes, @@ -3175,8 +3951,14 @@ private function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFu [], [], $this->afterExtractCall, - $this->parentScope + $this->parentScope, ); + + foreach ($parameterVariableExpressions as $expr) { + $scope = $scope->invalidateExpression($expr); + } + + return $scope; } public function isParameterValueNullable(Node\Param $parameter): bool @@ -3190,23 +3972,28 @@ public function isParameterValueNullable(Node\Param $parameter): bool /** * @api - * @param \PhpParser\Node\Name|\PhpParser\Node\Identifier|\PhpParser\Node\NullableType|\PhpParser\Node\UnionType|null $type - * @param bool $isNullable - * @param bool $isVariadic - * @return Type + * @param Node\Name|Node\Identifier|Node\ComplexType|null $type */ public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type { if ($isNullable) { return TypeCombinator::addNull( - $this->getFunctionType($type, false, $isVariadic) + $this->getFunctionType($type, false, $isVariadic), ); } if ($isVariadic) { + if ($this->phpVersion->supportsNamedArguments()) { + return new ArrayType(new UnionType([new IntegerType(), new StringType()]), $this->getFunctionType( + $type, + false, + false, + )); + } + return new ArrayType(new IntegerType(), $this->getFunctionType( $type, false, - false + false, )); } @@ -3214,7 +4001,7 @@ public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type $className = (string) $type; $lowercasedClassName = strtolower($className); if ($lowercasedClassName === 'parent') { - if ($this->isInClass() && $this->getClassReflection()->getParentClass() !== false) { + if ($this->isInClass() && $this->getClassReflection()->getParentClass() !== null) { return new ObjectType($this->getClassReflection()->getParentClass()->getName()); } @@ -3222,7 +4009,7 @@ public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type } } - return ParserNodeTypeToPHPStanType::resolve($type, $this->isInClass() ? $this->getClassReflection()->getName() : null); + return ParserNodeTypeToPHPStanType::resolve($type, $this->isInClass() ? $this->getClassReflection() : null); } public function enterForeach(Expr $iteratee, string $valueName, ?string $keyName): self @@ -3250,15 +4037,11 @@ public function enterForeachKey(Expr $iteratee, string $keyName): self } /** - * @param \PhpParser\Node\Name[] $classes - * @param string|null $variableName - * @return self + * @param Node\Name[] $classes */ public function enterCatch(array $classes, ?string $variableName): self { - $type = TypeCombinator::union(...array_map(static function (\PhpParser\Node\Name $class): ObjectType { - return new ObjectType((string) $class); - }, $classes)); + $type = TypeCombinator::union(...array_map(static fn (Node\Name $class): ObjectType => new ObjectType((string) $class), $classes)); return $this->enterCatchType($type, $variableName); } @@ -3271,7 +4054,7 @@ public function enterCatchType(Type $catchType, ?string $variableName): self return $this->assignVariable( $variableName, - TypeCombinator::intersect($catchType, new ObjectType(\Throwable::class)) + TypeCombinator::intersect($catchType, new ObjectType(Throwable::class)), ); } @@ -3297,7 +4080,7 @@ public function enterExpressionAssign(Expr $expr): self $this->nativeExpressionTypes, [], $this->afterExtractCall, - $this->parentScope + $this->parentScope, ); } @@ -3323,7 +4106,7 @@ public function exitExpressionAssign(Expr $expr): self $this->nativeExpressionTypes, [], $this->afterExtractCall, - $this->parentScope + $this->parentScope, ); } @@ -3339,7 +4122,7 @@ public function assignVariable(string $variableName, Type $type, ?TrinaryLogic $ if ($certainty === null) { $certainty = TrinaryLogic::createYes(); } elseif ($certainty->no()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $variableTypes = $this->getVariableTypes(); $variableTypes[$variableName] = new VariableTypeHolder($type, $certainty); @@ -3350,7 +4133,7 @@ public function assignVariable(string $variableName, Type $type, ?TrinaryLogic $ $variableString = $this->printer->prettyPrintExpr(new Variable($variableName)); $moreSpecificTypeHolders = $this->moreSpecificTypes; foreach (array_keys($moreSpecificTypeHolders) as $key) { - $matches = \Nette\Utils\Strings::matchAll((string) $key, '#\$[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*#'); + $matches = Strings::matchAll((string) $key, '#\$[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*#'); if ($matches === []) { continue; @@ -3399,7 +4182,7 @@ public function assignVariable(string $variableName, Type $type, ?TrinaryLogic $ $nativeTypes, $this->inFunctionCallsStack, $this->afterExtractCall, - $this->parentScope + $this->parentScope, ); } @@ -3435,34 +4218,17 @@ public function unsetExpression(Expr $expr): self $nativeTypes, [], $this->afterExtractCall, - $this->parentScope + $this->parentScope, ); } elseif ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) { - $varType = $this->getType($expr->var); - $constantArrays = TypeUtils::getConstantArrays($varType); - if (count($constantArrays) > 0) { - $unsetArrays = []; - $dimType = $this->getType($expr->dim); - foreach ($constantArrays as $constantArray) { - $unsetArrays[] = $constantArray->unsetOffset($dimType); - } - return $this->specifyExpressionType( - $expr->var, - TypeCombinator::union(...$unsetArrays) - ); - } - - $args = [new Node\Arg($expr->var)]; - - $arrays = TypeUtils::getArrays($varType); - $scope = $this; - if (count($arrays) > 0) { - $scope = $scope->specifyExpressionType($expr->var, TypeCombinator::union(...$arrays)); - } - - return $scope->invalidateExpression($expr->var) - ->invalidateExpression(new FuncCall(new Name\FullyQualified('count'), $args)) - ->invalidateExpression(new FuncCall(new Name('count'), $args)); + return $this->specifyExpressionType( + $expr->var, + $this->getType($expr->var)->unsetOffset($this->getType($expr->dim)), + )->invalidateExpression( + new FuncCall(new FullyQualified('count'), [new Arg($expr->var)]), + )->invalidateExpression( + new FuncCall(new FullyQualified('sizeof'), [new Arg($expr->var)]), + ); } return $this; @@ -3495,7 +4261,7 @@ public function specifyExpressionType(Expr $expr, Type $type, ?Type $nativeType $this->nativeExpressionTypes, $this->inFunctionCallsStack, $this->afterExtractCall, - $this->parentScope + $this->parentScope, ); } @@ -3533,7 +4299,7 @@ public function specifyExpressionType(Expr $expr, Type $type, ?Type $nativeType $nativeTypes, $this->inFunctionCallsStack, $this->afterExtractCall, - $this->parentScope + $this->parentScope, ); } elseif ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) { $constantArrays = TypeUtils::getConstantArrays($this->getType($expr->var)); @@ -3545,7 +4311,7 @@ public function specifyExpressionType(Expr $expr, Type $type, ?Type $nativeType } $scope = $this->specifyExpressionType( $expr->var, - TypeCombinator::union(...$setArrays) + TypeCombinator::union(...$setArrays), ); } } @@ -3569,7 +4335,10 @@ public function specifyExpressionType(Expr $expr, Type $type, ?Type $nativeType public function assignExpression(Expr $expr, Type $type): self { $scope = $this; - if ($expr instanceof PropertyFetch || $expr instanceof Expr\StaticPropertyFetch) { + if ($expr instanceof PropertyFetch) { + $scope = $this->invalidateExpression($expr) + ->invalidateMethodsOnExpression($expr->var); + } elseif ($expr instanceof Expr\StaticPropertyFetch) { $scope = $this->invalidateExpression($expr); } @@ -3579,31 +4348,104 @@ public function assignExpression(Expr $expr, Type $type): self public function invalidateExpression(Expr $expressionToInvalidate, bool $requireMoreCharacters = false): self { $exprStringToInvalidate = $this->getNodeKey($expressionToInvalidate); + $expressionToInvalidateClass = get_class($expressionToInvalidate); $moreSpecificTypeHolders = $this->moreSpecificTypes; $nativeExpressionTypes = $this->nativeExpressionTypes; $invalidated = false; + $nodeFinder = new NodeFinder(); foreach (array_keys($moreSpecificTypeHolders) as $exprString) { $exprString = (string) $exprString; - if (Strings::startsWith($exprString, $exprStringToInvalidate)) { - if ($exprString === $exprStringToInvalidate && $requireMoreCharacters) { - continue; + + try { + $expr = $this->parser->parseString('expr; + if ($exprExpr instanceof PropertyFetch) { + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($exprExpr, $this); + if ($propertyReflection !== null) { + $nativePropertyReflection = $propertyReflection->getNativeReflection(); + if ($nativePropertyReflection !== null && $nativePropertyReflection->isReadOnly()) { + continue; + } } - $nextLetter = substr($exprString, strlen($exprStringToInvalidate), 1); - if (Strings::match($nextLetter, '#[a-zA-Z_0-9\x7f-\xff]#') === null) { - unset($moreSpecificTypeHolders[$exprString]); - unset($nativeExpressionTypes[$exprString]); - $invalidated = true; - continue; + } + + $found = $nodeFinder->findFirst([$exprExpr], function (Node $node) use ($expressionToInvalidateClass, $exprStringToInvalidate): bool { + if (!$node instanceof $expressionToInvalidateClass) { + return false; } + + return $this->getNodeKey($node) === $exprStringToInvalidate; + }); + if ($found === null) { + continue; } - $matches = \Nette\Utils\Strings::matchAll($exprString, '#\$[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*#'); - if ($matches === []) { + + if ($requireMoreCharacters && $exprString === $exprStringToInvalidate) { continue; } - $matches = array_column($matches, 0); + unset($moreSpecificTypeHolders[$exprString]); + unset($nativeExpressionTypes[$exprString]); + $invalidated = true; + } + + if (!$invalidated) { + return $this; + } + + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->constantTypes, + $this->getFunction(), + $this->getNamespace(), + $this->getVariableTypes(), + $moreSpecificTypeHolders, + $this->conditionalExpressions, + $this->inClosureBindScopeClass, + $this->anonymousFunctionReflection, + $this->inFirstLevelStatement, + $this->currentlyAssignedExpressions, + $nativeExpressionTypes, + [], + $this->afterExtractCall, + $this->parentScope, + ); + } + + public function invalidateMethodsOnExpression(Expr $expressionToInvalidate): self + { + $exprStringToInvalidate = $this->getNodeKey($expressionToInvalidate); + $moreSpecificTypeHolders = $this->moreSpecificTypes; + $nativeExpressionTypes = $this->nativeExpressionTypes; + $invalidated = false; + $nodeFinder = new NodeFinder(); + foreach (array_keys($moreSpecificTypeHolders) as $exprString) { + $exprString = (string) $exprString; + + try { + $expr = $this->parser->parseString('findFirst([$expr->expr], function (Node $node) use ($exprStringToInvalidate): bool { + if (!$node instanceof MethodCall) { + return false; + } - if (!in_array($exprStringToInvalidate, $matches, true)) { + return $this->getNodeKey($node->var) === $exprStringToInvalidate; + }); + if ($found === null) { continue; } @@ -3632,7 +4474,7 @@ public function invalidateExpression(Expr $expressionToInvalidate, bool $require $nativeExpressionTypes, [], $this->afterExtractCall, - $this->parentScope + $this->parentScope, ); } @@ -3650,7 +4492,7 @@ public function removeTypeFromExpression(Expr $expr, Type $typeToRemove): self } $scope = $this->specifyExpressionType( $expr, - $typeAfterRemove + $typeAfterRemove, ); if ($expr instanceof Variable && is_string($expr->name)) { $scope->nativeExpressionTypes[sprintf('$%s', $expr->name)] = TypeCombinator::remove($this->getNativeType($expr), $typeToRemove); @@ -3661,24 +4503,38 @@ public function removeTypeFromExpression(Expr $expr, Type $typeToRemove): self /** * @api - * @param \PhpParser\Node\Expr $expr - * @return \PHPStan\Analyser\MutatingScope + * @return MutatingScope */ public function filterByTruthyValue(Expr $expr): Scope { + $exprString = $this->getNodeKey($expr); + if (array_key_exists($exprString, $this->truthyScopes)) { + return $this->truthyScopes[$exprString]; + } + $specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($this, $expr, TypeSpecifierContext::createTruthy()); - return $this->filterBySpecifiedTypes($specifiedTypes); + $scope = $this->filterBySpecifiedTypes($specifiedTypes); + $this->truthyScopes[$exprString] = $scope; + + return $scope; } /** * @api - * @param \PhpParser\Node\Expr $expr - * @return \PHPStan\Analyser\MutatingScope + * @return MutatingScope */ public function filterByFalseyValue(Expr $expr): Scope { + $exprString = $this->getNodeKey($expr); + if (array_key_exists($exprString, $this->falseyScopes)) { + return $this->falseyScopes[$exprString]; + } + $specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($this, $expr, TypeSpecifierContext::createFalsey()); - return $this->filterBySpecifiedTypes($specifiedTypes); + $scope = $this->filterBySpecifiedTypes($specifiedTypes); + $this->falseyScopes[$exprString] = $scope; + + return $scope; } public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self @@ -3702,6 +4558,7 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self } usort($typeSpecifications, static function (array $a, array $b): int { + // @phpstan-ignore-next-line $length = strlen((string) $a['exprString']) - strlen((string) $b['exprString']); if ($length !== 0) { return $length; @@ -3733,7 +4590,8 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self || !is_string($expr->name) || $specifiedTypes->shouldOverwrite() ) { - $match = \Nette\Utils\Strings::match((string) $typeSpecification['exprString'], '#^\$([a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*)#'); + // @phpstan-ignore-next-line + $match = Strings::match((string) $typeSpecification['exprString'], '#^\$([a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*)#'); if ($match !== null) { $skipVariables[$match[1]] = true; } @@ -3814,7 +4672,6 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self /** * @param array $newConditionalExpressionHolders - * @return self */ public function changeConditionalExpressions(array $newConditionalExpressionHolders): self { @@ -3834,14 +4691,12 @@ public function changeConditionalExpressions(array $newConditionalExpressionHold $this->nativeExpressionTypes, $this->inFunctionCallsStack, $this->afterExtractCall, - $this->parentScope + $this->parentScope, ); } /** - * @param string $exprString * @param ConditionalExpressionHolder[] $conditionalExpressionHolders - * @return self */ public function addConditionalExpressions(string $exprString, array $conditionalExpressionHolders): self { @@ -3863,7 +4718,7 @@ public function addConditionalExpressions(string $exprString, array $conditional $this->nativeExpressionTypes, $this->inFunctionCallsStack, $this->afterExtractCall, - $this->parentScope + $this->parentScope, ); } @@ -3885,7 +4740,7 @@ public function exitFirstLevelStatements(): self $this->nativeExpressionTypes, $this->inFunctionCallsStack, $this->afterExtractCall, - $this->parentScope + $this->parentScope, ); } @@ -3898,7 +4753,6 @@ public function isInFirstLevelStatement(): bool /** * @phpcsSuppress SlevomatCodingStandard.Classes.UnusedPrivateElements.UnusedMethod * @param Type[] $types - * @return self */ private function addMoreSpecificTypes(array $types): self { @@ -3923,7 +4777,7 @@ private function addMoreSpecificTypes(array $types): self $this->nativeExpressionTypes, [], $this->afterExtractCall, - $this->parentScope + $this->parentScope, ); } @@ -3933,16 +4787,10 @@ public function mergeWith(?self $otherScope): self return $this; } - $variableHolderToType = static function (VariableTypeHolder $holder): Type { - return $holder->getType(); - }; - $typeToVariableHolder = static function (Type $type): VariableTypeHolder { - return new VariableTypeHolder($type, TrinaryLogic::createYes()); - }; + $variableHolderToType = static fn (VariableTypeHolder $holder): Type => $holder->getType(); + $typeToVariableHolder = static fn (Type $type): VariableTypeHolder => new VariableTypeHolder($type, TrinaryLogic::createYes()); - $filterVariableHolders = static function (VariableTypeHolder $holder): bool { - return $holder->getCertainty()->yes(); - }; + $filterVariableHolders = static fn (VariableTypeHolder $holder): bool => $holder->getCertainty()->yes(); $ourVariableTypes = $this->getVariableTypes(); $theirVariableTypes = $otherScope->getVariableTypes(); @@ -3970,20 +4818,20 @@ public function mergeWith(?self $otherScope): self $conditionalExpressions, $ourVariableTypes, $theirVariableTypes, - $mergedVariableHolders + $mergedVariableHolders, ); $conditionalExpressions = $this->createConditionalExpressions( $conditionalExpressions, $theirVariableTypes, $ourVariableTypes, - $mergedVariableHolders + $mergedVariableHolders, ); return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), array_map($variableHolderToType, array_filter($this->mergeVariableHolders( array_map($typeToVariableHolder, $this->constantTypes), - array_map($typeToVariableHolder, $otherScope->constantTypes) + array_map($typeToVariableHolder, $otherScope->constantTypes), ), $filterVariableHolders)), $this->getFunction(), $this->getNamespace(), @@ -3996,11 +4844,11 @@ public function mergeWith(?self $otherScope): self [], array_map($variableHolderToType, array_filter($this->mergeVariableHolders( array_map($typeToVariableHolder, $this->nativeExpressionTypes), - array_map($typeToVariableHolder, $otherScope->nativeExpressionTypes) + array_map($typeToVariableHolder, $otherScope->nativeExpressionTypes), ), $filterVariableHolders)), [], $this->afterExtractCall && $otherScope->afterExtractCall, - $this->parentScope + $this->parentScope, ); } @@ -4040,7 +4888,7 @@ private function createConditionalExpressions( array $conditionalExpressions, array $variableTypes, array $theirVariableTypes, - array $mergedVariableHolders + array $mergedVariableHolders, ): array { $newVariableTypes = $variableTypes; @@ -4136,15 +4984,9 @@ private function mergeVariableHolders(array $ourVariableTypeHolders, array $thei public function processFinallyScope(self $finallyScope, self $originalFinallyScope): self { - $variableHolderToType = static function (VariableTypeHolder $holder): Type { - return $holder->getType(); - }; - $typeToVariableHolder = static function (Type $type): VariableTypeHolder { - return new VariableTypeHolder($type, TrinaryLogic::createYes()); - }; - $filterVariableHolders = static function (VariableTypeHolder $holder): bool { - return $holder->getCertainty()->yes(); - }; + $variableHolderToType = static fn (VariableTypeHolder $holder): Type => $holder->getType(); + $typeToVariableHolder = static fn (Type $type): VariableTypeHolder => new VariableTypeHolder($type, TrinaryLogic::createYes()); + $filterVariableHolders = static fn (VariableTypeHolder $holder): bool => $holder->getCertainty()->yes(); return $this->scopeFactory->create( $this->context, @@ -4152,19 +4994,19 @@ public function processFinallyScope(self $finallyScope, self $originalFinallySco array_map($variableHolderToType, array_filter($this->processFinallyScopeVariableTypeHolders( array_map($typeToVariableHolder, $this->constantTypes), array_map($typeToVariableHolder, $finallyScope->constantTypes), - array_map($typeToVariableHolder, $originalFinallyScope->constantTypes) + array_map($typeToVariableHolder, $originalFinallyScope->constantTypes), ), $filterVariableHolders)), $this->getFunction(), $this->getNamespace(), $this->processFinallyScopeVariableTypeHolders( $this->getVariableTypes(), $finallyScope->getVariableTypes(), - $originalFinallyScope->getVariableTypes() + $originalFinallyScope->getVariableTypes(), ), $this->processFinallyScopeVariableTypeHolders( $this->moreSpecificTypes, $finallyScope->moreSpecificTypes, - $originalFinallyScope->moreSpecificTypes + $originalFinallyScope->moreSpecificTypes, ), $this->conditionalExpressions, $this->inClosureBindScopeClass, @@ -4174,11 +5016,11 @@ public function processFinallyScope(self $finallyScope, self $originalFinallySco array_map($variableHolderToType, array_filter($this->processFinallyScopeVariableTypeHolders( array_map($typeToVariableHolder, $this->nativeExpressionTypes), array_map($typeToVariableHolder, $finallyScope->nativeExpressionTypes), - array_map($typeToVariableHolder, $originalFinallyScope->nativeExpressionTypes) + array_map($typeToVariableHolder, $originalFinallyScope->nativeExpressionTypes), ), $filterVariableHolders)), [], $this->afterExtractCall, - $this->parentScope + $this->parentScope, ); } @@ -4191,7 +5033,7 @@ public function processFinallyScope(self $finallyScope, self $originalFinallySco private function processFinallyScopeVariableTypeHolders( array $ourVariableTypeHolders, array $finallyVariableTypeHolders, - array $originalVariableTypeHolders + array $originalVariableTypeHolders, ): array { foreach ($finallyVariableTypeHolders as $name => $variableTypeHolder) { @@ -4214,15 +5056,12 @@ private function processFinallyScopeVariableTypeHolders( } /** - * @param self $closureScope - * @param self|null $prevScope * @param Expr\ClosureUse[] $byRefUses - * @return self */ public function processClosureScope( self $closureScope, ?self $prevScope, - array $byRefUses + array $byRefUses, ): self { $nativeExpressionTypes = $this->nativeExpressionTypes; @@ -4233,7 +5072,7 @@ public function processClosureScope( foreach ($byRefUses as $use) { if (!is_string($use->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $variableName = $use->var->name; @@ -4274,7 +5113,7 @@ public function processClosureScope( $nativeExpressionTypes, $this->inFunctionCallsStack, $this->afterExtractCall, - $this->parentScope + $this->parentScope, ); } @@ -4291,7 +5130,7 @@ public function processAlwaysIterableForeachScopeWithoutPollute(self $finalScope $variableTypeHolders[$name] = new VariableTypeHolder( $variableTypeHolder->getType(), - $variableTypeHolder->getCertainty()->and($variableTypeHolders[$name]->getCertainty()) + $variableTypeHolder->getCertainty()->and($variableTypeHolders[$name]->getCertainty()), ); } @@ -4304,7 +5143,7 @@ public function processAlwaysIterableForeachScopeWithoutPollute(self $finalScope $moreSpecificTypes[$exprString] = new VariableTypeHolder( $variableTypeHolder->getType(), - $variableTypeHolder->getCertainty()->and($moreSpecificTypes[$exprString]->getCertainty()) + $variableTypeHolder->getCertainty()->and($moreSpecificTypes[$exprString]->getCertainty()), ); } @@ -4324,7 +5163,7 @@ public function processAlwaysIterableForeachScopeWithoutPollute(self $finalScope $nativeTypes, [], $this->afterExtractCall, - $this->parentScope + $this->parentScope, ); } @@ -4332,26 +5171,20 @@ public function generalizeWith(self $otherScope): self { $variableTypeHolders = $this->generalizeVariableTypeHolders( $this->getVariableTypes(), - $otherScope->getVariableTypes() + $otherScope->getVariableTypes(), ); $moreSpecificTypes = $this->generalizeVariableTypeHolders( $this->moreSpecificTypes, - $otherScope->moreSpecificTypes + $otherScope->moreSpecificTypes, ); - $variableHolderToType = static function (VariableTypeHolder $holder): Type { - return $holder->getType(); - }; - $typeToVariableHolder = static function (Type $type): VariableTypeHolder { - return new VariableTypeHolder($type, TrinaryLogic::createYes()); - }; - $filterVariableHolders = static function (VariableTypeHolder $holder): bool { - return $holder->getCertainty()->yes(); - }; + $variableHolderToType = static fn (VariableTypeHolder $holder): Type => $holder->getType(); + $typeToVariableHolder = static fn (Type $type): VariableTypeHolder => new VariableTypeHolder($type, TrinaryLogic::createYes()); + $filterVariableHolders = static fn (VariableTypeHolder $holder): bool => $holder->getCertainty()->yes(); $nativeTypes = array_map($variableHolderToType, array_filter($this->generalizeVariableTypeHolders( array_map($typeToVariableHolder, $this->nativeExpressionTypes), - array_map($typeToVariableHolder, $otherScope->nativeExpressionTypes) + array_map($typeToVariableHolder, $otherScope->nativeExpressionTypes), ), $filterVariableHolders)); return $this->scopeFactory->create( @@ -4359,7 +5192,7 @@ public function generalizeWith(self $otherScope): self $this->isDeclareStrictTypes(), array_map($variableHolderToType, array_filter($this->generalizeVariableTypeHolders( array_map($typeToVariableHolder, $this->constantTypes), - array_map($typeToVariableHolder, $otherScope->constantTypes) + array_map($typeToVariableHolder, $otherScope->constantTypes), ), $filterVariableHolders)), $this->getFunction(), $this->getNamespace(), @@ -4373,7 +5206,7 @@ public function generalizeWith(self $otherScope): self $nativeTypes, [], $this->afterExtractCall, - $this->parentScope + $this->parentScope, ); } @@ -4384,7 +5217,7 @@ public function generalizeWith(self $otherScope): self */ private function generalizeVariableTypeHolders( array $variableTypeHolders, - array $otherVariableTypeHolders + array $otherVariableTypeHolders, ): array { foreach ($variableTypeHolders as $name => $variableTypeHolder) { @@ -4394,7 +5227,7 @@ private function generalizeVariableTypeHolders( $variableTypeHolders[$name] = new VariableTypeHolder( self::generalizeType($variableTypeHolder->getType(), $otherVariableTypeHolders[$name]->getType()), - $variableTypeHolder->getCertainty() + $variableTypeHolder->getCertainty(), ); } @@ -4413,6 +5246,7 @@ private static function generalizeType(Type $a, Type $b): Type $constantStrings = ['a' => [], 'b' => []]; $constantArrays = ['a' => [], 'b' => []]; $generalArrays = ['a' => [], 'b' => []]; + $integerRanges = ['a' => [], 'b' => []]; $otherTypes = []; foreach ([ @@ -4444,6 +5278,10 @@ private static function generalizeType(Type $a, Type $b): Type $generalArrays[$key][] = $type; continue; } + if ($type instanceof IntegerRangeType) { + $integerRanges[$key][] = $type; + continue; + } $otherTypes[] = $type; } @@ -4451,15 +5289,16 @@ private static function generalizeType(Type $a, Type $b): Type $resultTypes = []; foreach ([ - $constantIntegers, $constantFloats, $constantBooleans, $constantStrings, ] as $constantTypes) { if (count($constantTypes['a']) === 0) { + if (count($constantTypes['b']) > 0) { + $resultTypes[] = TypeCombinator::union(...$constantTypes['b']); + } continue; - } - if (count($constantTypes['b']) === 0) { + } elseif (count($constantTypes['b']) === 0) { $resultTypes[] = TypeCombinator::union(...$constantTypes['a']); continue; } @@ -4471,7 +5310,7 @@ private static function generalizeType(Type $a, Type $b): Type continue; } - $resultTypes[] = TypeUtils::generalizeType($constantTypes['a'][0]); + $resultTypes[] = TypeCombinator::union(...$constantTypes['a'], ...$constantTypes['b'])->generalize(GeneralizePrecision::moreSpecific()); } if (count($constantArrays['a']) > 0) { @@ -4487,8 +5326,8 @@ private static function generalizeType(Type $a, Type $b): Type $keyType, self::generalizeType( $constantArraysA->getOffsetValueType($keyType), - $constantArraysB->getOffsetValueType($keyType) - ) + $constantArraysB->getOffsetValueType($keyType), + ), ); } @@ -4496,10 +5335,12 @@ private static function generalizeType(Type $a, Type $b): Type } else { $resultTypes[] = new ArrayType( TypeCombinator::union(self::generalizeType($constantArraysA->getIterableKeyType(), $constantArraysB->getIterableKeyType())), - TypeCombinator::union(self::generalizeType($constantArraysA->getIterableValueType(), $constantArraysB->getIterableValueType())) + TypeCombinator::union(self::generalizeType($constantArraysA->getIterableValueType(), $constantArraysB->getIterableValueType())), ); } } + } elseif (count($constantArrays['b']) > 0) { + $resultTypes[] = TypeCombinator::union(...$constantArrays['b']); } if (count($generalArrays['a']) > 0) { @@ -4532,9 +5373,142 @@ private static function generalizeType(Type $a, Type $b): Type $resultTypes[] = new ArrayType( TypeCombinator::union(self::generalizeType($generalArraysA->getIterableKeyType(), $generalArraysB->getIterableKeyType())), - TypeCombinator::union(self::generalizeType($aValueType, $bValueType)) + TypeCombinator::union(self::generalizeType($aValueType, $bValueType)), ); } + } elseif (count($generalArrays['b']) > 0) { + $resultTypes[] = TypeCombinator::union(...$generalArrays['b']); + } + + if (count($constantIntegers['a']) > 0) { + if (count($constantIntegers['b']) === 0) { + $resultTypes[] = TypeCombinator::union(...$constantIntegers['a']); + } else { + $constantIntegersA = TypeCombinator::union(...$constantIntegers['a']); + $constantIntegersB = TypeCombinator::union(...$constantIntegers['b']); + + if ($constantIntegersA->equals($constantIntegersB)) { + $resultTypes[] = $constantIntegersA; + } else { + $min = null; + $max = null; + foreach ($constantIntegers['a'] as $int) { + if ($min === null || $int->getValue() < $min) { + $min = $int->getValue(); + } + if ($max !== null && $int->getValue() <= $max) { + continue; + } + + $max = $int->getValue(); + } + + $gotGreater = false; + $gotSmaller = false; + foreach ($constantIntegers['b'] as $int) { + if ($int->getValue() > $max) { + $gotGreater = true; + } + if ($int->getValue() >= $min) { + continue; + } + + $gotSmaller = true; + } + + if ($gotGreater && $gotSmaller) { + $resultTypes[] = new IntegerType(); + } elseif ($gotGreater) { + $resultTypes[] = IntegerRangeType::fromInterval($min, null); + } elseif ($gotSmaller) { + $resultTypes[] = IntegerRangeType::fromInterval(null, $max); + } else { + $resultTypes[] = TypeCombinator::union($constantIntegersA, $constantIntegersB); + } + } + } + } elseif (count($constantIntegers['b']) > 0) { + $resultTypes[] = TypeCombinator::union(...$constantIntegers['b']); + } + + if (count($integerRanges['a']) > 0) { + if (count($integerRanges['b']) === 0) { + $resultTypes[] = TypeCombinator::union(...$integerRanges['a']); + } else { + $integerRangesA = TypeCombinator::union(...$integerRanges['a']); + $integerRangesB = TypeCombinator::union(...$integerRanges['b']); + + if ($integerRangesA->equals($integerRangesB)) { + $resultTypes[] = $integerRangesA; + } else { + $min = null; + $max = null; + foreach ($integerRanges['a'] as $range) { + if ($range->getMin() === null) { + $rangeMin = PHP_INT_MIN; + } else { + $rangeMin = $range->getMin(); + } + if ($range->getMax() === null) { + $rangeMax = PHP_INT_MAX; + } else { + $rangeMax = $range->getMax(); + } + + if ($min === null || $rangeMin < $min) { + $min = $rangeMin; + } + if ($max !== null && $rangeMax <= $max) { + continue; + } + + $max = $rangeMax; + } + + $gotGreater = false; + $gotSmaller = false; + foreach ($integerRanges['b'] as $range) { + if ($range->getMin() === null) { + $rangeMin = PHP_INT_MIN; + } else { + $rangeMin = $range->getMin(); + } + if ($range->getMax() === null) { + $rangeMax = PHP_INT_MAX; + } else { + $rangeMax = $range->getMax(); + } + + if ($rangeMax > $max) { + $gotGreater = true; + } + if ($rangeMin >= $min) { + continue; + } + + $gotSmaller = true; + } + + if ($min === PHP_INT_MIN) { + $min = null; + } + if ($max === PHP_INT_MAX) { + $max = null; + } + + if ($gotGreater && $gotSmaller) { + $resultTypes[] = new IntegerType(); + } elseif ($gotGreater) { + $resultTypes[] = IntegerRangeType::fromInterval($min, null); + } elseif ($gotSmaller) { + $resultTypes[] = IntegerRangeType::fromInterval(null, $max); + } else { + $resultTypes[] = TypeCombinator::union($integerRangesA, $integerRangesB); + } + } + } + } elseif (count($integerRanges['b']) > 0) { + $resultTypes[] = TypeCombinator::union(...$integerRanges['b']); } return TypeCombinator::union(...$resultTypes, ...$otherTypes); @@ -4571,13 +5545,11 @@ public function equals(self $otherScope): bool return false; } - $typeToVariableHolder = static function (Type $type): VariableTypeHolder { - return new VariableTypeHolder($type, TrinaryLogic::createYes()); - }; + $typeToVariableHolder = static fn (Type $type): VariableTypeHolder => new VariableTypeHolder($type, TrinaryLogic::createYes()); $nativeExpressionTypesResult = $this->compareVariableTypeHolders( array_map($typeToVariableHolder, $this->nativeExpressionTypes), - array_map($typeToVariableHolder, $otherScope->nativeExpressionTypes) + array_map($typeToVariableHolder, $otherScope->nativeExpressionTypes), ); if (!$nativeExpressionTypesResult) { @@ -4586,14 +5558,13 @@ public function equals(self $otherScope): bool return $this->compareVariableTypeHolders( array_map($typeToVariableHolder, $this->constantTypes), - array_map($typeToVariableHolder, $otherScope->constantTypes) + array_map($typeToVariableHolder, $otherScope->constantTypes), ); } /** * @param VariableTypeHolder[] $variableTypeHolders * @param VariableTypeHolder[] $otherVariableTypeHolders - * @return bool */ private function compareVariableTypeHolders(array $variableTypeHolders, array $otherVariableTypeHolders): bool { @@ -4686,7 +5657,7 @@ public function debug(): array $key = sprintf( '%s-specified (%s)', $exprString, - $typeHolder->getCertainty()->describe() + $typeHolder->getCertainty()->describe(), ); $descriptions[$key] = $typeHolder->getType()->describe(VerbosityLevel::precise()); } @@ -4724,7 +5695,7 @@ private function exactInstantiation(New_ $node, string $className): ?Type $methodCall = new Expr\StaticCall( new Name($resolvedClassName), new Node\Identifier($constructorMethod->getName()), - $node->args + $node->getArgs(), ); foreach ($this->dynamicReturnTypeExtensionRegistry->getDynamicStaticMethodReturnTypeExtensionsForClass($classReflection->getName()) as $dynamicStaticMethodReturnTypeExtension) { @@ -4732,7 +5703,16 @@ private function exactInstantiation(New_ $node, string $className): ?Type continue; } - $resolvedTypes[] = $dynamicStaticMethodReturnTypeExtension->getTypeFromStaticMethodCall($constructorMethod, $methodCall, $this); + $resolvedType = $dynamicStaticMethodReturnTypeExtension->getTypeFromStaticMethodCall( + $constructorMethod, + $methodCall, + $this, + ); + if ($resolvedType === null) { + continue; + } + + $resolvedTypes[] = $resolvedType; } if (count($resolvedTypes) > 0) { @@ -4785,19 +5765,46 @@ private function exactInstantiation(New_ $node, string $className): ?Type if ($constructorMethod instanceof DummyConstructorReflection || $constructorMethod->getDeclaringClass()->getName() !== $classReflection->getName()) { return new GenericObjectType( $resolvedClassName, - $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()) + $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()), ); } $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $this, - $methodCall->args, - $constructorMethod->getVariants() + $methodCall->getArgs(), + $constructorMethod->getVariants(), ); + if ($this->explicitMixedInUnknownGenericNew) { + return new GenericObjectType( + $resolvedClassName, + $classReflection->typeMapToList($parametersAcceptor->getResolvedTemplateTypeMap()), + ); + } + + $resolvedPhpDoc = $classReflection->getResolvedPhpDoc(); + if ($resolvedPhpDoc === null) { + return $objectType; + } + + $list = []; + $typeMap = $parametersAcceptor->getResolvedTemplateTypeMap(); + foreach ($resolvedPhpDoc->getTemplateTags() as $tag) { + $templateType = $typeMap->getType($tag->getName()); + if ($templateType !== null) { + $list[] = $templateType; + continue; + } + $bound = $tag->getBound(); + if ($bound instanceof MixedType && $bound->isExplicitMixed()) { + $bound = new MixedType(false); + } + $list[] = $bound; + } + return new GenericObjectType( $resolvedClassName, - $classReflection->typeMapToList($parametersAcceptor->getResolvedTemplateTypeMap()) + $list, ); } @@ -4807,12 +5814,12 @@ private function getTypeToInstantiateForNew(Type $type): Type if ($type instanceof ConstantStringType) { return new ObjectType($type->getValue()); } - if ($type instanceof TypeWithClassName) { - return $type; - } if ($type instanceof GenericClassStringType) { return $type->getGenericType(); } + if ((new ObjectWithoutClassType())->isSuperTypeOf($type)->yes()) { + return $type; + } return null; }; @@ -4821,11 +5828,7 @@ private function getTypeToInstantiateForNew(Type $type): Type foreach ($type->getTypes() as $innerType) { $decidedType = $decideType($innerType); if ($decidedType === null) { - if ($this->objectFromNewClass) { - return new ObjectWithoutClassType(); - } - - return new MixedType(false, new StringType()); + return new ObjectWithoutClassType(); } $types[] = $decidedType; @@ -4836,11 +5839,7 @@ private function getTypeToInstantiateForNew(Type $type): Type $decidedType = $decideType($type); if ($decidedType === null) { - if ($this->objectFromNewClass) { - return new ObjectWithoutClassType(); - } - - return new MixedType(false, new StringType()); + return new ObjectWithoutClassType(); } return $decidedType; @@ -4872,10 +5871,7 @@ public function getMethodReflection(Type $typeWithMethod, string $methodName): ? } /** - * @param \PHPStan\Type\Type $typeWithMethod - * @param string $methodName - * @param MethodCall|\PhpParser\Node\Expr\StaticCall $methodCall - * @return \PHPStan\Type\Type|null + * @param MethodCall|Node\Expr\StaticCall $methodCall */ private function methodCallReturnType(Type $typeWithMethod, string $methodName, Expr $methodCall): ?Type { @@ -4892,7 +5888,12 @@ private function methodCallReturnType(Type $typeWithMethod, string $methodName, continue; } - $resolvedTypes[] = $dynamicMethodReturnTypeExtension->getTypeFromMethodCall($methodReflection, $methodCall, $this); + $resolvedType = $dynamicMethodReturnTypeExtension->getTypeFromMethodCall($methodReflection, $methodCall, $this); + if ($resolvedType === null) { + continue; + } + + $resolvedTypes[] = $resolvedType; } } else { foreach ($this->dynamicReturnTypeExtensionRegistry->getDynamicStaticMethodReturnTypeExtensionsForClass($className) as $dynamicStaticMethodReturnTypeExtension) { @@ -4900,7 +5901,16 @@ private function methodCallReturnType(Type $typeWithMethod, string $methodName, continue; } - $resolvedTypes[] = $dynamicStaticMethodReturnTypeExtension->getTypeFromStaticMethodCall($methodReflection, $methodCall, $this); + $resolvedType = $dynamicStaticMethodReturnTypeExtension->getTypeFromStaticMethodCall( + $methodReflection, + $methodCall, + $this, + ); + if ($resolvedType === null) { + continue; + } + + $resolvedTypes[] = $resolvedType; } } } @@ -4911,8 +5921,8 @@ private function methodCallReturnType(Type $typeWithMethod, string $methodName, return ParametersAcceptorSelector::selectFromArgs( $this, - $methodCall->args, - $methodReflection->getVariants() + $methodCall->getArgs(), + $methodReflection->getVariants(), )->getReturnType(); } @@ -4941,10 +5951,7 @@ public function getPropertyReflection(Type $typeWithProperty, string $propertyNa } /** - * @param \PHPStan\Type\Type $fetchedOnType - * @param string $propertyName - * @param PropertyFetch|\PhpParser\Node\Expr\StaticPropertyFetch $propertyFetch - * @return \PHPStan\Type\Type|null + * @param PropertyFetch|Node\Expr\StaticPropertyFetch $propertyFetch */ private function propertyFetchType(Type $fetchedOnType, string $propertyName, Expr $propertyFetch): ?Type { @@ -4960,4 +5967,188 @@ private function propertyFetchType(Type $fetchedOnType, string $propertyName, Ex return $propertyReflection->getReadableType(); } + /** + * @param ConstantIntegerType|IntegerRangeType $range + * @param Node\Expr\AssignOp\Div|Node\Expr\AssignOp\Minus|Node\Expr\AssignOp\Mul|Node\Expr\AssignOp\Plus|Node\Expr\BinaryOp\Div|Node\Expr\BinaryOp\Minus|Node\Expr\BinaryOp\Mul|Node\Expr\BinaryOp\Plus $node + * @param IntegerRangeType|ConstantIntegerType|UnionType $operand + */ + private function integerRangeMath(Type $range, Expr $node, Type $operand): Type + { + if ($range instanceof IntegerRangeType) { + $rangeMin = $range->getMin(); + $rangeMax = $range->getMax(); + } else { + $rangeMin = $range->getValue(); + $rangeMax = $rangeMin; + } + + if ($operand instanceof UnionType) { + + $unionParts = []; + + foreach ($operand->getTypes() as $type) { + if ($type instanceof IntegerRangeType || $type instanceof ConstantIntegerType) { + $unionParts[] = $this->integerRangeMath($range, $node, $type); + } else { + $unionParts[] = $type->toNumber(); + } + } + + $union = TypeCombinator::union(...$unionParts); + if ($operand instanceof BenevolentUnionType) { + return TypeUtils::toBenevolentUnion($union)->toNumber(); + } + + return $union->toNumber(); + } + + if ($node instanceof Node\Expr\BinaryOp\Plus || $node instanceof Node\Expr\AssignOp\Plus) { + if ($operand instanceof ConstantIntegerType) { + /** @var int|float|null $min */ + $min = $rangeMin !== null ? $rangeMin + $operand->getValue() : null; + + /** @var int|float|null $max */ + $max = $rangeMax !== null ? $rangeMax + $operand->getValue() : null; + } else { + /** @var int|float|null $min */ + $min = $rangeMin !== null && $operand->getMin() !== null ? $rangeMin + $operand->getMin() : null; + + /** @var int|float|null $max */ + $max = $rangeMax !== null && $operand->getMax() !== null ? $rangeMax + $operand->getMax() : null; + } + } elseif ($node instanceof Node\Expr\BinaryOp\Minus || $node instanceof Node\Expr\AssignOp\Minus) { + if ($operand instanceof ConstantIntegerType) { + /** @var int|float|null $min */ + $min = $rangeMin !== null ? $rangeMin - $operand->getValue() : null; + + /** @var int|float|null $max */ + $max = $rangeMax !== null ? $rangeMax - $operand->getValue() : null; + } else { + if ($rangeMin === $rangeMax && $rangeMin !== null + && ($operand->getMin() === null || $operand->getMax() === null)) { + $min = null; + $max = $rangeMin; + } else { + if ($operand->getMin() === null) { + $min = null; + } elseif ($rangeMin !== null) { + if ($operand->getMax() !== null) { + /** @var int|float $min */ + $min = $rangeMin - $operand->getMax(); + } else { + /** @var int|float $min */ + $min = $rangeMin - $operand->getMin(); + } + } else { + $min = null; + } + + if ($operand->getMax() === null) { + $min = null; + $max = null; + } elseif ($rangeMax !== null) { + if ($rangeMin !== null && $operand->getMin() === null) { + /** @var int|float $min */ + $min = $rangeMin - $operand->getMax(); + $max = null; + } elseif ($operand->getMin() !== null) { + /** @var int|float $max */ + $max = $rangeMax - $operand->getMin(); + } else { + $max = null; + } + } else { + $max = null; + } + + if ($min !== null && $max !== null && $min > $max) { + [$min, $max] = [$max, $min]; + } + } + } + } elseif ($node instanceof Node\Expr\BinaryOp\Mul || $node instanceof Node\Expr\AssignOp\Mul) { + if ($operand instanceof ConstantIntegerType) { + /** @var int|float|null $min */ + $min = $rangeMin !== null ? $rangeMin * $operand->getValue() : null; + + /** @var int|float|null $max */ + $max = $rangeMax !== null ? $rangeMax * $operand->getValue() : null; + } else { + /** @var int|float|null $min */ + $min = $rangeMin !== null && $operand->getMin() !== null ? $rangeMin * $operand->getMin() : null; + + /** @var int|float|null $max */ + $max = $rangeMax !== null && $operand->getMax() !== null ? $rangeMax * $operand->getMax() : null; + } + + if ($min !== null && $max !== null && $min > $max) { + [$min, $max] = [$max, $min]; + } + + // invert maximas on multiplication with negative constants + if ((($range instanceof ConstantIntegerType && $range->getValue() < 0) + || ($operand instanceof ConstantIntegerType && $operand->getValue() < 0)) + && ($min === null || $max === null)) { + [$min, $max] = [$max, $min]; + } + + } else { + if ($operand instanceof ConstantIntegerType) { + $min = $rangeMin !== null && $operand->getValue() !== 0 ? $rangeMin / $operand->getValue() : null; + $max = $rangeMax !== null && $operand->getValue() !== 0 ? $rangeMax / $operand->getValue() : null; + } else { + $min = $rangeMin !== null && $operand->getMin() !== null && $operand->getMin() !== 0 ? $rangeMin / $operand->getMin() : null; + $max = $rangeMax !== null && $operand->getMax() !== null && $operand->getMax() !== 0 ? $rangeMax / $operand->getMax() : null; + } + + if ($range instanceof IntegerRangeType && $operand instanceof IntegerRangeType) { + if ($rangeMax === null && $operand->getMax() === null) { + $min = 0; + } elseif ($rangeMin === null && $operand->getMin() === null) { + $min = null; + $max = null; + } + } + + if ($operand instanceof IntegerRangeType + && ($operand->getMin() === null || $operand->getMax() === null) + || ($rangeMin === null || $rangeMax === null) + || is_float($min) || is_float($max) + ) { + if (is_float($min)) { + $min = (int) $min; + } + if (is_float($max)) { + $max = (int) $max; + } + + if ($min !== null && $max !== null && $min > $max) { + [$min, $max] = [$max, $min]; + } + + // invert maximas on division with negative constants + if ((($range instanceof ConstantIntegerType && $range->getValue() < 0) + || ($operand instanceof ConstantIntegerType && $operand->getValue() < 0)) + && ($min === null || $max === null)) { + [$min, $max] = [$max, $min]; + } + + if ($min === null && $max === null) { + return new BenevolentUnionType([new IntegerType(), new FloatType()]); + } + + return TypeCombinator::union(IntegerRangeType::fromInterval($min, $max), new FloatType()); + } + } + + if (is_float($min)) { + $min = null; + } + if (is_float($max)) { + $max = null; + } + + return IntegerRangeType::fromInterval($min, $max); + } + } diff --git a/src/Analyser/NameScope.php b/src/Analyser/NameScope.php index 146f6954c5..683732b28c 100644 --- a/src/Analyser/NameScope.php +++ b/src/Analyser/NameScope.php @@ -5,43 +5,31 @@ use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Type; +use function array_key_exists; +use function array_merge; +use function array_shift; +use function count; +use function explode; +use function implode; +use function ltrim; +use function sprintf; +use function strpos; +use function strtolower; /** @api */ class NameScope { - private ?string $namespace; - - /** @var array alias(string) => fullName(string) */ - private array $uses; - - private ?string $className; - - private ?string $functionName; - private TemplateTypeMap $templateTypeMap; - /** @var array */ - private array $typeAliasesMap; - - private bool $bypassTypeAliases; - /** * @api - * @param string|null $namespace * @param array $uses alias(string) => fullName(string) - * @param string|null $className * @param array $typeAliasesMap */ - public function __construct(?string $namespace, array $uses, ?string $className = null, ?string $functionName = null, ?TemplateTypeMap $templateTypeMap = null, array $typeAliasesMap = [], bool $bypassTypeAliases = false) + 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) { - $this->namespace = $namespace; - $this->uses = $uses; - $this->className = $className; - $this->functionName = $functionName; $this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty(); - $this->typeAliasesMap = $typeAliasesMap; - $this->bypassTypeAliases = $bypassTypeAliases; } public function getNamespace(): ?string @@ -119,7 +107,7 @@ public function resolveTemplateTypeName(string $name): ?Type public function withTemplateTypeMap(TemplateTypeMap $map): self { - if ($map->isEmpty()) { + if ($map->isEmpty() && $this->templateTypeMap->isEmpty()) { return $this; } @@ -130,9 +118,9 @@ public function withTemplateTypeMap(TemplateTypeMap $map): self $this->functionName, new TemplateTypeMap(array_merge( $this->templateTypeMap->getTypes(), - $map->getTypes() + $map->getTypes(), )), - $this->typeAliasesMap + $this->typeAliasesMap, ); } @@ -149,7 +137,7 @@ public function unsetTemplateType(string $name): self $this->className, $this->functionName, $this->templateTypeMap->unsetType($name), - $this->typeAliasesMap + $this->typeAliasesMap, ); } @@ -170,7 +158,6 @@ public function hasTypeAlias(string $alias): bool /** * @param mixed[] $properties - * @return self */ public static function __set_state(array $properties): self { @@ -180,7 +167,7 @@ public static function __set_state(array $properties): self $properties['className'], $properties['functionName'], $properties['templateTypeMap'], - $properties['typeAliasesMap'] + $properties['typeAliasesMap'], ); } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index eb1a1b497d..e72ffe3a90 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2,6 +2,8 @@ namespace PHPStan\Analyser; +use ArrayAccess; +use Closure; use PhpParser\Comment\Doc; use PhpParser\Node; use PhpParser\Node\Expr; @@ -47,7 +49,8 @@ use PhpParser\Node\Stmt\Unset_; use PhpParser\Node\Stmt\While_; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass; -use PHPStan\BetterReflection\Reflector\ClassReflector; +use PHPStan\BetterReflection\Reflection\ReflectionEnum; +use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection; use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource; use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider; @@ -56,6 +59,7 @@ use PHPStan\File\FileReader; use PHPStan\Node\BooleanAndNode; use PHPStan\Node\BooleanOrNode; +use PHPStan\Node\BreaklessWhileLoopNode; use PHPStan\Node\CatchWithUnthrownExceptionNode; use PHPStan\Node\ClassConstantsNode; use PHPStan\Node\ClassMethodsNode; @@ -63,40 +67,50 @@ use PHPStan\Node\ClassPropertyNode; use PHPStan\Node\ClassStatementsGatherer; use PHPStan\Node\ClosureReturnStatementsNode; +use PHPStan\Node\DoWhileLoopConditionNode; use PHPStan\Node\ExecutionEndNode; +use PHPStan\Node\Expr\GetIterableValueTypeExpr; +use PHPStan\Node\Expr\GetOffsetValueTypeExpr; +use PHPStan\Node\Expr\OriginalPropertyTypeExpr; +use PHPStan\Node\Expr\SetOffsetValueTypeExpr; use PHPStan\Node\FinallyExitPointsNode; +use PHPStan\Node\FunctionCallableNode; use PHPStan\Node\FunctionReturnStatementsNode; use PHPStan\Node\InArrowFunctionNode; use PHPStan\Node\InClassMethodNode; use PHPStan\Node\InClassNode; use PHPStan\Node\InClosureNode; +use PHPStan\Node\InForeachNode; use PHPStan\Node\InFunctionNode; +use PHPStan\Node\InstantiationCallableNode; use PHPStan\Node\LiteralArrayItem; use PHPStan\Node\LiteralArrayNode; use PHPStan\Node\MatchExpressionArm; use PHPStan\Node\MatchExpressionArmCondition; use PHPStan\Node\MatchExpressionNode; +use PHPStan\Node\MethodCallableNode; use PHPStan\Node\MethodReturnStatementsNode; +use PHPStan\Node\PropertyAssignNode; use PHPStan\Node\ReturnStatement; +use PHPStan\Node\StaticMethodCallableNode; use PHPStan\Node\UnreachableStatementNode; use PHPStan\Parser\Parser; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\PhpDoc\ResolvedPhpDocBlock; +use PHPStan\PhpDoc\StubPhpDocProvider; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\Native\NativeMethodReflection; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\PassedByReference; -use PHPStan\Reflection\Php\DummyParameter; use PHPStan\Reflection\Php\PhpMethodReflection; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; -use PHPStan\Type\CallableType; use PHPStan\Type\ClosureType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; @@ -105,6 +119,7 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ErrorType; use PHPStan\Type\FileTypeMapper; +use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\IntegerType; @@ -119,8 +134,30 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; +use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; use PHPStan\Type\VoidType; +use Throwable; +use Traversable; +use TypeError; +use function array_fill_keys; +use function array_filter; +use function array_key_exists; +use function array_map; +use function array_merge; +use function array_pop; +use function array_reverse; +use function array_slice; +use function base64_decode; +use function count; +use function in_array; +use function is_array; +use function is_int; +use function is_string; +use function sprintf; +use function strtolower; +use function trim; +use const PHP_VERSION_ID; class NodeScopeResolver { @@ -128,98 +165,32 @@ class NodeScopeResolver private const LOOP_SCOPE_ITERATIONS = 3; private const GENERALIZE_AFTER_ITERATION = 1; - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private ClassReflector $classReflector; - - private ClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider; - - private \PHPStan\Parser\Parser $parser; - - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - private PhpVersion $phpVersion; - - private \PHPStan\PhpDoc\PhpDocInheritanceResolver $phpDocInheritanceResolver; - - private \PHPStan\File\FileHelper $fileHelper; - - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; - - private DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider; - - private bool $polluteScopeWithLoopInitialAssignments; - - private bool $polluteCatchScopeWithTryAssignments; - - private bool $polluteScopeWithAlwaysIterableForeach; - - /** @var string[][] className(string) => methods(string[]) */ - private array $earlyTerminatingMethodCalls; - - /** @var array */ - private array $earlyTerminatingFunctionCalls; - - private bool $implicitThrows; - - private bool $preciseExceptionTracking; - /** @var bool[] filePath(string) => bool(true) */ private array $analysedFiles = []; /** - * @param \PHPStan\Reflection\ReflectionProvider $reflectionProvider - * @param ClassReflector $classReflector - * @param Parser $parser - * @param FileTypeMapper $fileTypeMapper - * @param PhpDocInheritanceResolver $phpDocInheritanceResolver - * @param FileHelper $fileHelper - * @param TypeSpecifier $typeSpecifier - * @param bool $polluteScopeWithLoopInitialAssignments - * @param bool $polluteCatchScopeWithTryAssignments - * @param bool $polluteScopeWithAlwaysIterableForeach * @param string[][] $earlyTerminatingMethodCalls className(string) => methods(string[]) * @param array $earlyTerminatingFunctionCalls - * @param bool $implicitThrows - * @param bool $preciseExceptionTracking */ public function __construct( - ReflectionProvider $reflectionProvider, - ClassReflector $classReflector, - ClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider, - Parser $parser, - FileTypeMapper $fileTypeMapper, - PhpVersion $phpVersion, - PhpDocInheritanceResolver $phpDocInheritanceResolver, - FileHelper $fileHelper, - TypeSpecifier $typeSpecifier, - DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider, - bool $polluteScopeWithLoopInitialAssignments, - bool $polluteCatchScopeWithTryAssignments, - bool $polluteScopeWithAlwaysIterableForeach, - array $earlyTerminatingMethodCalls, - array $earlyTerminatingFunctionCalls, - bool $implicitThrows, - bool $preciseExceptionTracking + private ReflectionProvider $reflectionProvider, + private Reflector $reflector, + private ClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider, + private Parser $parser, + private FileTypeMapper $fileTypeMapper, + private StubPhpDocProvider $stubPhpDocProvider, + private PhpVersion $phpVersion, + private PhpDocInheritanceResolver $phpDocInheritanceResolver, + private FileHelper $fileHelper, + private TypeSpecifier $typeSpecifier, + private DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider, + private bool $polluteScopeWithLoopInitialAssignments, + private bool $polluteScopeWithAlwaysIterableForeach, + private array $earlyTerminatingMethodCalls, + private array $earlyTerminatingFunctionCalls, + private bool $implicitThrows, ) { - $this->reflectionProvider = $reflectionProvider; - $this->classReflector = $classReflector; - $this->classReflectionExtensionRegistryProvider = $classReflectionExtensionRegistryProvider; - $this->parser = $parser; - $this->fileTypeMapper = $fileTypeMapper; - $this->phpVersion = $phpVersion; - $this->phpDocInheritanceResolver = $phpDocInheritanceResolver; - $this->fileHelper = $fileHelper; - $this->typeSpecifier = $typeSpecifier; - $this->dynamicThrowTypeExtensionProvider = $dynamicThrowTypeExtensionProvider; - $this->polluteScopeWithLoopInitialAssignments = $polluteScopeWithLoopInitialAssignments; - $this->polluteCatchScopeWithTryAssignments = $polluteCatchScopeWithTryAssignments; - $this->polluteScopeWithAlwaysIterableForeach = $polluteScopeWithAlwaysIterableForeach; - $this->earlyTerminatingMethodCalls = $earlyTerminatingMethodCalls; - $this->earlyTerminatingFunctionCalls = $earlyTerminatingFunctionCalls; - $this->implicitThrows = $implicitThrows; - $this->preciseExceptionTracking = $preciseExceptionTracking; } /** @@ -233,14 +204,13 @@ public function setAnalysedFiles(array $files): void /** * @api - * @param \PhpParser\Node[] $nodes - * @param \PHPStan\Analyser\MutatingScope $scope - * @param callable(\PhpParser\Node $node, Scope $scope): void $nodeCallback + * @param Node[] $nodes + * @param callable(Node $node, Scope $scope): void $nodeCallback */ public function processNodes( array $nodes, MutatingScope $scope, - callable $nodeCallback + callable $nodeCallback, ): void { $nodesCount = count($nodes); @@ -268,17 +238,14 @@ public function processNodes( } /** - * @param \PhpParser\Node $parentNode - * @param \PhpParser\Node\Stmt[] $stmts - * @param \PHPStan\Analyser\MutatingScope $scope - * @param callable(\PhpParser\Node $node, Scope $scope): void $nodeCallback - * @return StatementResult + * @param Node\Stmt[] $stmts + * @param callable(Node $node, Scope $scope): void $nodeCallback */ public function processStmtNodes( Node $parentNode, array $stmts, MutatingScope $scope, - callable $nodeCallback + callable $nodeCallback, ): StatementResult { $exitPoints = []; @@ -294,7 +261,7 @@ public function processStmtNodes( $statementResult = $this->processStmtNode( $stmt, $scope, - $nodeCallback + $nodeCallback, ); $scope = $statementResult->getScope(); $hasYield = $hasYield || $statementResult->hasYield(); @@ -309,9 +276,9 @@ public function processStmtNodes( $hasYield, $statementResult->isAlwaysTerminating(), $statementResult->getExitPoints(), - $statementResult->getThrowPoints() + $statementResult->getThrowPoints(), ), - $parentNode->returnType !== null + $parentNode->returnType !== null, ), $scope); } @@ -337,7 +304,7 @@ public function processStmtNodes( $nodeCallback(new ExecutionEndNode( $parentNode, $statementResult, - $parentNode->returnType !== null + $parentNode->returnType !== null, ), $scope); } @@ -345,15 +312,12 @@ public function processStmtNodes( } /** - * @param \PhpParser\Node\Stmt $stmt - * @param \PHPStan\Analyser\MutatingScope $scope - * @param callable(\PhpParser\Node $node, Scope $scope): void $nodeCallback - * @return StatementResult + * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function processStmtNode( Node\Stmt $stmt, MutatingScope $scope, - callable $nodeCallback + callable $nodeCallback, ): StatementResult { if ( @@ -365,13 +329,15 @@ private function processStmtNode( !$stmt instanceof Static_ && !$stmt instanceof Foreach_ && !$stmt instanceof Node\Stmt\Global_ + && !$stmt instanceof Node\Stmt\Property + && !$stmt instanceof Node\Stmt\PropertyProperty ) { $scope = $this->processStmtVarAnnotation($scope, $stmt, null); } if ($stmt instanceof Node\Stmt\ClassMethod) { if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ( $scope->isInTrait() @@ -416,7 +382,7 @@ private function processStmtNode( foreach ($stmt->attrGroups as $attrGroup) { foreach ($attrGroup->attrs as $attr) { foreach ($attr->args as $arg) { - $nodeCallback($arg->value, $scope); + $this->processExprNode($arg->value, $scope, $nodeCallback, ExpressionContext::createDeep()); } } } @@ -440,12 +406,13 @@ private function processStmtNode( $isDeprecated, $isInternal, $isFinal, - $isPure + $isPure, ); $nodeCallback(new InFunctionNode($stmt), $functionScope); $gatheredReturnStatements = []; - $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $functionScope, static function (\PhpParser\Node $node, Scope $scope) use ($nodeCallback, $functionScope, &$gatheredReturnStatements): void { + $executionEnds = []; + $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $functionScope, static function (Node $node, Scope $scope) use ($nodeCallback, $functionScope, &$gatheredReturnStatements, &$executionEnds): void { $nodeCallback($node, $scope); if ($scope->getFunction() !== $functionScope->getFunction()) { return; @@ -453,6 +420,10 @@ private function processStmtNode( if ($scope->isInAnonymousFunction()) { return; } + if ($node instanceof ExecutionEndNode) { + $executionEnds[] = $node; + return; + } if (!$node instanceof Return_) { return; } @@ -463,7 +434,8 @@ private function processStmtNode( $nodeCallback(new FunctionReturnStatementsNode( $stmt, $gatheredReturnStatements, - $statementResult + $statementResult, + $executionEnds, ), $functionScope); } elseif ($stmt instanceof Node\Stmt\ClassMethod) { $hasYield = false; @@ -471,7 +443,7 @@ private function processStmtNode( foreach ($stmt->attrGroups as $attrGroup) { foreach ($attrGroup->attrs as $attr) { foreach ($attr->args as $arg) { - $nodeCallback($arg->value, $scope); + $this->processExprNode($arg->value, $scope, $nodeCallback, ExpressionContext::createDeep()); } } } @@ -495,7 +467,7 @@ private function processStmtNode( $isDeprecated, $isInternal, $isFinal, - $isPure + $isPure, ); if ($stmt->name->toLowerString() === '__construct') { @@ -505,7 +477,7 @@ private function processStmtNode( } if (!$param->var instanceof Variable || !is_string($param->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $phpDoc = null; if ($param->getDocComment() !== null) { @@ -515,10 +487,10 @@ private function processStmtNode( $param->var->name, $param->flags, $param->type, - $param->default, + null, $phpDoc, true, - $param + $param, ), $methodScope); } } @@ -529,7 +501,8 @@ private function processStmtNode( if ($stmt->stmts !== null) { $gatheredReturnStatements = []; - $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $methodScope, static function (\PhpParser\Node $node, Scope $scope) use ($nodeCallback, $methodScope, &$gatheredReturnStatements): void { + $executionEnds = []; + $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $methodScope, static function (Node $node, Scope $scope) use ($nodeCallback, $methodScope, &$gatheredReturnStatements, &$executionEnds): void { $nodeCallback($node, $scope); if ($scope->getFunction() !== $methodScope->getFunction()) { return; @@ -537,6 +510,10 @@ private function processStmtNode( if ($scope->isInAnonymousFunction()) { return; } + if ($node instanceof ExecutionEndNode) { + $executionEnds[] = $node; + return; + } if (!$node instanceof Return_) { return; } @@ -546,7 +523,8 @@ private function processStmtNode( $nodeCallback(new MethodReturnStatementsNode( $stmt, $gatheredReturnStatements, - $statementResult + $statementResult, + $executionEnds, ), $methodScope); } } elseif ($stmt instanceof Echo_) { @@ -595,7 +573,7 @@ private function processStmtNode( $scope = $scope->filterBySpecifiedTypes($this->typeSpecifier->specifyTypesInCondition( $scope, $stmt->expr, - TypeSpecifierContext::createNull() + TypeSpecifierContext::createNull(), )); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); @@ -619,12 +597,12 @@ private function processStmtNode( $hasYield = false; $throwPoints = []; if (isset($stmt->namespacedName)) { - $classReflection = $this->getCurrentClassReflection($stmt, $scope); + $classReflection = $this->getCurrentClassReflection($stmt, $stmt->namespacedName->toString(), $scope); $classScope = $scope->enterClass($classReflection); $nodeCallback(new InClassNode($stmt, $classReflection), $classScope); } elseif ($stmt instanceof Class_) { if ($stmt->name === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ($stmt->getAttribute('anonymousClass', false) === false) { $classReflection = $this->reflectionProvider->getClass($stmt->name->toString()); @@ -634,13 +612,13 @@ private function processStmtNode( $classScope = $scope->enterClass($classReflection); $nodeCallback(new InClassNode($stmt, $classReflection), $classScope); } else { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } foreach ($stmt->attrGroups as $attrGroup) { foreach ($attrGroup->attrs as $attr) { foreach ($attr->args as $arg) { - $nodeCallback($arg->value, $classScope); + $this->processExprNode($arg->value, $classScope, $nodeCallback, ExpressionContext::createDeep()); } } } @@ -656,7 +634,7 @@ private function processStmtNode( foreach ($stmt->attrGroups as $attrGroup) { foreach ($attrGroup->attrs as $attr) { foreach ($attr->args as $arg) { - $nodeCallback($arg->value, $scope); + $this->processExprNode($arg->value, $scope, $nodeCallback, ExpressionContext::createDeep()); } } } @@ -671,9 +649,9 @@ private function processStmtNode( $prop->default, $docComment !== null ? $docComment->getText() : null, false, - $prop + $prop, ), - $scope + $scope, ); } @@ -789,8 +767,13 @@ private function processStmtNode( $scope = $condResult->getScope(); $arrayComparisonExpr = new BinaryOp\NotIdentical( $stmt->expr, - new Array_([]) + new Array_([]), ); + $inForeachScope = $scope; + if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) { + $inForeachScope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt); + } + $nodeCallback(new InForeachNode($stmt), $inForeachScope); $bodyScope = $this->enterForeach($scope->filterByTruthyValue($arrayComparisonExpr), $stmt); $count = 0; do { @@ -809,7 +792,7 @@ private function processStmtNode( } if ($count >= self::GENERALIZE_AFTER_ITERATION) { - $bodyScope = $bodyScope->generalizeWith($prevScope); + $bodyScope = $prevScope->generalizeWith($bodyScope); } $count++; } while (!$alwaysTerminating && $count < self::LOOP_SCOPE_ITERATIONS); @@ -842,13 +825,16 @@ private function processStmtNode( if (!$isIterableAtLeastOnce->no()) { $throwPoints = array_merge($throwPoints, $finalScopeResult->getThrowPoints()); } + if (!(new ObjectType(Traversable::class))->isSuperTypeOf($scope->getType($stmt->expr))->no()) { + $throwPoints[] = ThrowPoint::createImplicit($scope, $stmt->expr); + } return new StatementResult( $finalScope, $finalScopeResult->hasYield() || $condResult->hasYield(), $isIterableAtLeastOnce->yes() && $finalScopeResult->isAlwaysTerminating(), $finalScopeResult->getExitPointsForOuterLoop(), - $throwPoints + $throwPoints, ); } elseif ($stmt instanceof While_) { $condResult = $this->processExprNode($stmt->cond, $scope, static function (): void { @@ -872,7 +858,7 @@ private function processStmtNode( } if ($count >= self::GENERALIZE_AFTER_ITERATION) { - $bodyScope = $bodyScope->generalizeWith($prevScope); + $bodyScope = $prevScope->generalizeWith($bodyScope); } $count++; } while (!$alwaysTerminating && $count < self::LOOP_SCOPE_ITERATIONS); @@ -881,7 +867,7 @@ private function processStmtNode( $bodyScopeMaybeRan = $bodyScope; $bodyScope = $this->processExprNode($stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); $finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback)->filterOutLoopExitPoints(); - $finalScope = $finalScopeResult->getScope(); + $finalScope = $finalScopeResult->getScope()->filterByFalseyValue($stmt->cond); foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { $finalScope = $finalScope->mergeWith($continueExitPoint->getScope()); } @@ -895,6 +881,7 @@ private function processStmtNode( $isIterableAtLeastOnce = $beforeCondBooleanType instanceof ConstantBooleanType && $beforeCondBooleanType->getValue(); $alwaysIterates = $condBooleanType instanceof ConstantBooleanType && $condBooleanType->getValue(); $neverIterates = $condBooleanType instanceof ConstantBooleanType && !$condBooleanType->getValue(); + $nodeCallback(new BreaklessWhileLoopNode($stmt, $finalScopeResult->getExitPoints()), $bodyScopeMaybeRan); if ($alwaysIterates) { $isAlwaysTerminating = count($finalScopeResult->getExitPointsByType(Break_::class)) === 0; @@ -921,7 +908,7 @@ private function processStmtNode( $finalScopeResult->hasYield() || $condResult->hasYield(), $isAlwaysTerminating, $finalScopeResult->getExitPointsForOuterLoop(), - $throwPoints + $throwPoints, ); } elseif ($stmt instanceof Do_) { $finalScope = null; @@ -950,7 +937,7 @@ private function processStmtNode( } if ($count >= self::GENERALIZE_AFTER_ITERATION) { - $bodyScope = $bodyScope->generalizeWith($prevScope); + $bodyScope = $prevScope->generalizeWith($bodyScope); } $count++; } while (!$alwaysTerminating && $count < self::LOOP_SCOPE_ITERATIONS); @@ -959,17 +946,19 @@ private function processStmtNode( $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback)->filterOutLoopExitPoints(); $bodyScope = $bodyScopeResult->getScope(); + foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { + $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); + } $condBooleanType = $bodyScope->getType($stmt->cond)->toBoolean(); $alwaysIterates = $condBooleanType instanceof ConstantBooleanType && $condBooleanType->getValue(); + $nodeCallback(new DoWhileLoopConditionNode($stmt->cond, $bodyScopeResult->getExitPoints()), $bodyScope); + if ($alwaysIterates) { $alwaysTerminating = count($bodyScopeResult->getExitPointsByType(Break_::class)) === 0; } else { $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating(); } - foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { - $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); - } $finalScope = $alwaysTerminating ? $finalScope : $bodyScope->mergeWith($finalScope); if ($finalScope === null) { $finalScope = $scope; @@ -979,6 +968,8 @@ private function processStmtNode( $hasYield = $condResult->hasYield(); $throwPoints = $condResult->getThrowPoints(); $finalScope = $condResult->getFalseyScope(); + } else { + $this->processExprNode($stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep()); } foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); @@ -989,7 +980,7 @@ private function processStmtNode( $bodyScopeResult->hasYield() || $hasYield, $alwaysTerminating, $bodyScopeResult->getExitPointsForOuterLoop(), - array_merge($throwPoints, $bodyScopeResult->getThrowPoints()) + array_merge($throwPoints, $bodyScopeResult->getThrowPoints()), ); } elseif ($stmt instanceof For_) { $initScope = $scope; @@ -1003,9 +994,18 @@ private function processStmtNode( } $bodyScope = $initScope; + $isIterableAtLeastOnce = TrinaryLogic::createYes(); foreach ($stmt->cond as $condExpr) { $condResult = $this->processExprNode($condExpr, $bodyScope, static function (): void { }, ExpressionContext::createDeep()); + $initScope = $condResult->getScope(); + $condTruthiness = $condResult->getScope()->getType($condExpr)->toBoolean(); + if ($condTruthiness instanceof ConstantBooleanType) { + $condTruthinessTrinary = TrinaryLogic::createFromBoolean($condTruthiness->getValue()); + } else { + $condTruthinessTrinary = TrinaryLogic::createMaybe(); + } + $isIterableAtLeastOnce = $isIterableAtLeastOnce->and($condTruthinessTrinary); $hasYield = $hasYield || $condResult->hasYield(); $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints()); $bodyScope = $condResult->getTruthyScope(); @@ -1039,7 +1039,7 @@ private function processStmtNode( } if ($count >= self::GENERALIZE_AFTER_ITERATION) { - $bodyScope = $bodyScope->generalizeWith($prevScope); + $bodyScope = $prevScope->generalizeWith($bodyScope); } $count++; } while (!$alwaysTerminating && $count < self::LOOP_SCOPE_ITERATIONS); @@ -1054,25 +1054,45 @@ private function processStmtNode( foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { $finalScope = $continueExitPoint->getScope()->mergeWith($finalScope); } + + $loopScope = $finalScope; foreach ($stmt->loop as $loopExpr) { - $finalScope = $this->processExprNode($loopExpr, $finalScope, $nodeCallback, ExpressionContext::createTopLevel())->getScope(); + $loopScope = $this->processExprNode($loopExpr, $loopScope, $nodeCallback, ExpressionContext::createTopLevel())->getScope(); + } + $finalScope = $finalScope->generalizeWith($loopScope); + foreach ($stmt->cond as $condExpr) { + $finalScope = $finalScope->filterByFalseyValue($condExpr); } + foreach ($finalScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); } - if ($this->polluteScopeWithLoopInitialAssignments) { - $scope = $initScope; - } + if ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating()) { + if ($this->polluteScopeWithLoopInitialAssignments) { + $finalScope = $initScope; + } else { + $finalScope = $scope; + } - $finalScope = $finalScope->mergeWith($scope); + } elseif ($isIterableAtLeastOnce->maybe()) { + if ($this->polluteScopeWithLoopInitialAssignments) { + $finalScope = $finalScope->mergeWith($initScope); + } else { + $finalScope = $finalScope->mergeWith($scope); + } + } else { + if (!$this->polluteScopeWithLoopInitialAssignments) { + $finalScope = $finalScope->mergeWith($scope); + } + } return new StatementResult( $finalScope, $finalScopeResult->hasYield() || $hasYield, false/* $finalScopeResult->isAlwaysTerminating() && $isAlwaysIterable*/, $finalScopeResult->getExitPointsForOuterLoop(), - array_merge($throwPoints, $finalScopeResult->getThrowPoints()) + array_merge($throwPoints, $finalScopeResult->getThrowPoints()), ); } elseif ($stmt instanceof Switch_) { $condResult = $this->processExprNode($stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep()); @@ -1146,6 +1166,7 @@ private function processStmtNode( $finalScope = $branchScopeResult->isAlwaysTerminating() ? null : $branchScope; $exitPoints = []; + $finallyExitPoints = []; $alwaysTerminating = $branchScopeResult->isAlwaysTerminating(); $hasYield = $branchScopeResult->hasYield(); @@ -1155,6 +1176,7 @@ private function processStmtNode( $finallyScope = null; } foreach ($branchScopeResult->getExitPoints() as $exitPoint) { + $finallyExitPoints[] = $exitPoint; if ($exitPoint->getStatement() instanceof Throw_) { continue; } @@ -1171,89 +1193,76 @@ private function processStmtNode( foreach ($stmt->catches as $catchNode) { $nodeCallback($catchNode, $scope); - if ($this->preciseExceptionTracking || !$this->polluteCatchScopeWithTryAssignments) { - $catchType = TypeCombinator::union(...array_map(static function (Name $name): Type { - return new ObjectType($name->toString()); - }, $catchNode->types)); - $originalCatchType = $catchType; - $catchType = TypeCombinator::remove($catchType, $pastCatchTypes); - $pastCatchTypes = TypeCombinator::union($pastCatchTypes, $originalCatchType); - $matchingThrowPoints = []; - $newThrowPoints = []; - foreach ($throwPoints as $throwPoint) { - if (!$throwPoint->isExplicit() && !$catchType->isSuperTypeOf(new ObjectType(\Throwable::class))->yes()) { - continue; - } - $isSuperType = $catchType->isSuperTypeOf($throwPoint->getType()); - if ($isSuperType->no()) { - continue; - } + $catchType = TypeCombinator::union(...array_map(static fn (Name $name): Type => new ObjectType($name->toString()), $catchNode->types)); + $originalCatchType = $catchType; + $catchType = TypeCombinator::remove($catchType, $pastCatchTypes); + $pastCatchTypes = TypeCombinator::union($pastCatchTypes, $originalCatchType); + $matchingThrowPoints = []; + $newThrowPoints = []; + foreach ($throwPoints as $throwPoint) { + if (!$throwPoint->isExplicit() && !$catchType->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) { + continue; + } + $isSuperType = $catchType->isSuperTypeOf($throwPoint->getType()); + if ($isSuperType->no()) { + continue; + } + $matchingThrowPoints[] = $throwPoint; + } + $hasExplicit = count($matchingThrowPoints) > 0; + foreach ($throwPoints as $throwPoint) { + $isSuperType = $catchType->isSuperTypeOf($throwPoint->getType()); + if (!$hasExplicit && !$isSuperType->no()) { $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; - } - $newThrowPoints[] = $throwPoint->subtractCatchType($catchType); + if ($isSuperType->yes()) { + continue; } - $throwPoints = $newThrowPoints; - - if (count($matchingThrowPoints) === 0) { - $throwableThrowPoints = []; - if ($originalCatchType->isSuperTypeOf(new ObjectType(\Throwable::class))->yes()) { - foreach ($branchScopeResult->getThrowPoints() as $originalThrowPoint) { - if (!$originalThrowPoint->canContainAnyThrowable()) { - continue; - } + $newThrowPoints[] = $throwPoint->subtractCatchType($catchType); + } + $throwPoints = $newThrowPoints; - $throwableThrowPoints[] = $originalThrowPoint; + if (count($matchingThrowPoints) === 0) { + $throwableThrowPoints = []; + if ($originalCatchType->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) { + foreach ($branchScopeResult->getThrowPoints() as $originalThrowPoint) { + if (!$originalThrowPoint->canContainAnyThrowable()) { + continue; } - } - if (count($throwableThrowPoints) === 0) { - $nodeCallback(new CatchWithUnthrownExceptionNode($catchNode, $catchType), $scope); - continue; + $throwableThrowPoints[] = $originalThrowPoint; } - - $matchingThrowPoints = $throwableThrowPoints; } - $catchScope = null; - foreach ($matchingThrowPoints as $matchingThrowPoint) { - if ($catchScope === null) { - $catchScope = $matchingThrowPoint->getScope(); - } else { - $catchScope = $catchScope->mergeWith($matchingThrowPoint->getScope()); - } + if (count($throwableThrowPoints) === 0) { + $nodeCallback(new CatchWithUnthrownExceptionNode($catchNode, $catchType, $originalCatchType), $scope); + continue; } - $variableName = null; - if ($catchNode->var !== null) { - if (!is_string($catchNode->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); - } + $matchingThrowPoints = $throwableThrowPoints; + } - $variableName = $catchNode->var->name; + $catchScope = null; + foreach ($matchingThrowPoints as $matchingThrowPoint) { + if ($catchScope === null) { + $catchScope = $matchingThrowPoint->getScope(); + } else { + $catchScope = $catchScope->mergeWith($matchingThrowPoint->getScope()); } + } - $catchScopeResult = $this->processStmtNodes($catchNode, $catchNode->stmts, $catchScope->enterCatchType($catchType, $variableName), $nodeCallback); - $catchScopeForFinally = $catchScopeResult->getScope(); - } else { - $initialScope = $scope; - if (count($throwPoints) > 0) { - $initialScope = $throwPoints[0]->getScope(); + $variableName = null; + if ($catchNode->var !== null) { + if (!is_string($catchNode->var->name)) { + throw new ShouldNotHappenException(); } - $catchScopeForFinally = $this->processCatchNode($catchNode, $branchScope, $nodeCallback)->getScope(); - $catchScopeResult = $this->processCatchNode($catchNode, $initialScope->mergeWith($branchScope), static function (): void { - }); + $variableName = $catchNode->var->name; } + $catchScopeResult = $this->processStmtNodes($catchNode, $catchNode->stmts, $catchScope->enterCatchType($catchType, $variableName), $nodeCallback); + $catchScopeForFinally = $catchScopeResult->getScope(); + $finalScope = $catchScopeResult->isAlwaysTerminating() ? $finalScope : $catchScopeResult->getScope()->mergeWith($finalScope); $alwaysTerminating = $alwaysTerminating && $catchScopeResult->isAlwaysTerminating(); $hasYield = $hasYield || $catchScopeResult->hasYield(); @@ -1264,6 +1273,7 @@ private function processStmtNode( $finallyScope = $finallyScope->mergeWith($catchScopeForFinally); } foreach ($catchScopeResult->getExitPoints() as $exitPoint) { + $finallyExitPoints[] = $exitPoint; if ($exitPoint->getStatement() instanceof Throw_) { continue; } @@ -1303,7 +1313,7 @@ private function processStmtNode( if (count($finallyResult->getExitPoints()) > 0) { $nodeCallback(new FinallyExitPointsNode( $finallyResult->getExitPoints(), - $exitPoints + $finallyExitPoints, ), $scope); } $exitPoints = array_merge($exitPoints, $finallyResult->getExitPoints()); @@ -1331,7 +1341,7 @@ private function processStmtNode( $vars = []; foreach ($stmt->vars as $var) { if (!$var instanceof Variable) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $scope = $this->lookForEnterVariableAssign($scope, $var); $this->processExprNode($var, $scope, $nodeCallback, ExpressionContext::createDeep()); @@ -1364,7 +1374,7 @@ private function processStmtNode( $hasYield = false; $throwPoints = []; if (!is_string($stmt->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ($stmt->default !== null) { $this->processExprNode($stmt->default, $scope, $nodeCallback, ExpressionContext::createDeep()); @@ -1380,7 +1390,7 @@ private function processStmtNode( foreach ($stmt->attrGroups as $attrGroup) { foreach ($attrGroup->attrs as $attr) { foreach ($attr->args as $arg) { - $nodeCallback($arg->value, $scope); + $this->processExprNode($arg->value, $scope, $nodeCallback, ExpressionContext::createDeep()); } } } @@ -1408,8 +1418,6 @@ private function processStmtNode( } /** - * @param Node\Stmt $statement - * @param MutatingScope $scope * @return ThrowPoint[]|null */ private function getOverridingThrowPoints(Node\Stmt $statement, MutatingScope $scope): ?array @@ -1425,100 +1433,81 @@ private function getOverridingThrowPoints(Node\Stmt $statement, MutatingScope $s $scope->isInClass() ? $scope->getClassReflection()->getName() : null, $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, $function !== null ? $function->getName() : null, - $comment->getText() + $comment->getText(), ); $throwsTag = $resolvedPhpDoc->getThrowsTag(); if ($throwsTag !== null) { - return [ThrowPoint::createExplicit($scope, $throwsTag->getType(), $statement, false)]; + $throwsType = $throwsTag->getType(); + if ($throwsType instanceof VoidType) { + return []; + } + + return [ThrowPoint::createExplicit($scope, $throwsType, $statement, false)]; } } return null; } - private function getCurrentClassReflection(Node\Stmt\ClassLike $stmt, Scope $scope): ClassReflection + private function getCurrentClassReflection(Node\Stmt\ClassLike $stmt, string $className, Scope $scope): ClassReflection { - $className = $stmt->namespacedName->toString(); if (!$this->reflectionProvider->hasClass($className)) { - return $this->createAstClassReflection($stmt, $scope); + return $this->createAstClassReflection($stmt, $className, $scope); } - $defaultClassReflection = $this->reflectionProvider->getClass($stmt->namespacedName->toString()); + $defaultClassReflection = $this->reflectionProvider->getClass($className); if ($defaultClassReflection->getFileName() !== $scope->getFile()) { - return $this->createAstClassReflection($stmt, $scope); + return $this->createAstClassReflection($stmt, $className, $scope); } $startLine = $defaultClassReflection->getNativeReflection()->getStartLine(); if ($startLine !== $stmt->getStartLine()) { - return $this->createAstClassReflection($stmt, $scope); + return $this->createAstClassReflection($stmt, $className, $scope); } return $defaultClassReflection; } - private function createAstClassReflection(Node\Stmt\ClassLike $stmt, Scope $scope): ClassReflection + private function createAstClassReflection(Node\Stmt\ClassLike $stmt, string $className, Scope $scope): ClassReflection { $nodeToReflection = new NodeToReflection(); $betterReflectionClass = $nodeToReflection->__invoke( - $this->classReflector, + $this->reflector, $stmt, - new LocatedSource(FileReader::read($scope->getFile()), $scope->getFile()), - $scope->getNamespace() !== null ? new Node\Stmt\Namespace_(new Name($scope->getNamespace())) : null + new LocatedSource(FileReader::read($scope->getFile()), $className, $scope->getFile()), + $scope->getNamespace() !== null ? new Node\Stmt\Namespace_(new Name($scope->getNamespace())) : null, ); if (!$betterReflectionClass instanceof \PHPStan\BetterReflection\Reflection\ReflectionClass) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } + $enumAdapter = base64_decode('UEhQU3RhblxCZXR0ZXJSZWZsZWN0aW9uXFJlZmxlY3Rpb25cQWRhcHRlclxSZWZsZWN0aW9uRW51bQ==', true); + return new ClassReflection( $this->reflectionProvider, $this->fileTypeMapper, + $this->stubPhpDocProvider, + $this->phpDocInheritanceResolver, $this->phpVersion, $this->classReflectionExtensionRegistryProvider->getRegistry()->getPropertiesClassReflectionExtensions(), $this->classReflectionExtensionRegistryProvider->getRegistry()->getMethodsClassReflectionExtensions(), $betterReflectionClass->getName(), - new ReflectionClass($betterReflectionClass), + $betterReflectionClass instanceof ReflectionEnum && PHP_VERSION_ID >= 80000 ? new $enumAdapter($betterReflectionClass) : new ReflectionClass($betterReflectionClass), null, null, null, - sprintf('%s:%d', $scope->getFile(), $stmt->getStartLine()) + sprintf('%s:%d', $scope->getFile(), $stmt->getStartLine()), ); } - /** - * @param Node\Stmt\Catch_ $catchNode - * @param MutatingScope $catchScope - * @param callable(\PhpParser\Node $node, Scope $scope): void $nodeCallback - * @return StatementResult - */ - private function processCatchNode( - Node\Stmt\Catch_ $catchNode, - MutatingScope $catchScope, - callable $nodeCallback - ): StatementResult - { - $variableName = null; - if ($catchNode->var !== null) { - if (!is_string($catchNode->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); - } - - $variableName = $catchNode->var->name; - } - - $catchScope = $catchScope->enterCatch($catchNode->types, $variableName); - return $this->processStmtNodes($catchNode, $catchNode->stmts, $catchScope, $nodeCallback); - } - private function lookForEnterVariableAssign(MutatingScope $scope, Expr $expr): MutatingScope { if (!$expr instanceof ArrayDimFetch || $expr->dim !== null) { $scope = $scope->enterExpressionAssign($expr); } if (!$expr instanceof Variable) { - return $this->lookForVariableAssignCallback($scope, $expr, static function (MutatingScope $scope, Expr $expr): MutatingScope { - return $scope->enterExpressionAssign($expr); - }); + return $this->lookForVariableAssignCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->enterExpressionAssign($expr)); } return $scope; @@ -1526,32 +1515,29 @@ private function lookForEnterVariableAssign(MutatingScope $scope, Expr $expr): M private function lookForExitVariableAssign(MutatingScope $scope, Expr $expr): MutatingScope { - $scope = $scope->exitExpressionAssign($expr); + if (!$expr instanceof ArrayDimFetch || $expr->dim !== null) { + $scope = $scope->exitExpressionAssign($expr); + } if (!$expr instanceof Variable) { - return $this->lookForVariableAssignCallback($scope, $expr, static function (MutatingScope $scope, Expr $expr): MutatingScope { - return $scope->exitExpressionAssign($expr); - }); + return $this->lookForVariableAssignCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->exitExpressionAssign($expr)); } return $scope; } /** - * @param MutatingScope $scope - * @param Expr $expr - * @param \Closure(MutatingScope $scope, Expr $expr): MutatingScope $callback - * @return MutatingScope + * @param Closure(MutatingScope $scope, Expr $expr): MutatingScope $callback */ - private function lookForVariableAssignCallback(MutatingScope $scope, Expr $expr, \Closure $callback): MutatingScope + private function lookForVariableAssignCallback(MutatingScope $scope, Expr $expr, Closure $callback): MutatingScope { if ($expr instanceof Variable) { $scope = $callback($scope, $expr); } elseif ($expr instanceof ArrayDimFetch) { - while ($expr instanceof ArrayDimFetch) { - $expr = $expr->var; + if ($expr->dim !== null) { + $scope = $callback($scope, $expr); } - $scope = $this->lookForVariableAssignCallback($scope, $expr, $callback); + $scope = $this->lookForVariableAssignCallback($scope, $expr->var, $callback); } elseif ($expr instanceof PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch) { $scope = $this->lookForVariableAssignCallback($scope, $expr->var, $callback); } elseif ($expr instanceof StaticPropertyFetch) { @@ -1580,14 +1566,14 @@ private function ensureShallowNonNullability(MutatingScope $scope, Expr $exprToS $scope = $scope->specifyExpressionType( $exprToSpecify, $exprTypeWithoutNull, - TypeCombinator::removeNull($nativeType) + TypeCombinator::removeNull($nativeType), ); return new EnsuredNonNullabilityResult( $scope, [ new EnsuredNonNullabilityResultExpression($exprToSpecify, $exprType, $nativeType), - ] + ], ); } @@ -1622,9 +1608,7 @@ private function ensureNonNullability(MutatingScope $scope, Expr $expr, bool $fi } /** - * @param MutatingScope $scope * @param EnsuredNonNullabilityResultExpression[] $specifiedExpressions - * @return MutatingScope */ private function revertNonNullability(MutatingScope $scope, array $specifiedExpressions): MutatingScope { @@ -1632,7 +1616,7 @@ private function revertNonNullability(MutatingScope $scope, array $specifiedExpr $scope = $scope->specifyExpressionType( $specifiedExpressionResult->getExpression(), $specifiedExpressionResult->getOriginalType(), - $specifiedExpressionResult->getOriginalNativeType() + $specifiedExpressionResult->getOriginalNativeType(), ); } @@ -1688,14 +1672,26 @@ private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr } /** - * @param \PhpParser\Node\Expr $expr - * @param \PHPStan\Analyser\MutatingScope $scope - * @param callable(\PhpParser\Node $node, Scope $scope): void $nodeCallback - * @param \PHPStan\Analyser\ExpressionContext $context - * @return \PHPStan\Analyser\ExpressionResult + * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function processExprNode(Expr $expr, MutatingScope $scope, callable $nodeCallback, ExpressionContext $context): ExpressionResult { + if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) { + if ($expr instanceof FuncCall) { + $newExpr = new FunctionCallableNode($expr->name, $expr->getAttributes()); + } elseif ($expr instanceof MethodCall) { + $newExpr = new MethodCallableNode($expr->var, $expr->name, $expr); + } elseif ($expr instanceof StaticCall) { + $newExpr = new StaticMethodCallableNode($expr->class, $expr->name, $expr); + } elseif ($expr instanceof New_ && !$expr->class instanceof Class_) { + $newExpr = new InstantiationCallableNode($expr->class, $expr->getAttributes()); + } else { + throw new ShouldNotHappenException(); + } + + return $this->processExprNode($newExpr, $scope, $nodeCallback, $context); + } + $this->callNodeCallbackWithExpression($nodeCallback, $expr, $scope, $context); if ($expr instanceof Variable) { @@ -1705,84 +1701,49 @@ private function processExprNode(Expr $expr, MutatingScope $scope, callable $nod return $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep()); } } elseif ($expr instanceof Assign || $expr instanceof AssignRef) { - if (!$expr->var instanceof Array_ && !$expr->var instanceof List_) { - $result = $this->processAssignVar( - $scope, - $expr->var, - $expr->expr, - $nodeCallback, - $context, - function (MutatingScope $scope) use ($expr, $nodeCallback, $context): ExpressionResult { - if ($expr instanceof AssignRef) { - $scope = $scope->enterExpressionAssign($expr->expr); - } + $result = $this->processAssignVar( + $scope, + $expr->var, + $expr->expr, + $nodeCallback, + $context, + function (MutatingScope $scope) use ($expr, $nodeCallback, $context): ExpressionResult { + if ($expr instanceof AssignRef) { + $scope = $scope->enterExpressionAssign($expr->expr); + } - if ($expr->var instanceof Variable && is_string($expr->var->name)) { - $context = $context->enterRightSideAssign( - $expr->var->name, - $scope->getType($expr->expr) - ); - } + if ($expr->var instanceof Variable && is_string($expr->var->name)) { + $context = $context->enterRightSideAssign( + $expr->var->name, + $scope->getType($expr->expr), + ); + } - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); - $hasYield = $result->hasYield(); - $throwPoints = $result->getThrowPoints(); - $scope = $result->getScope(); + $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); + $hasYield = $result->hasYield(); + $throwPoints = $result->getThrowPoints(); + $scope = $result->getScope(); - if ($expr instanceof AssignRef) { - $scope = $scope->exitExpressionAssign($expr->expr); - } + if ($expr instanceof AssignRef) { + $scope = $scope->exitExpressionAssign($expr->expr); + } - return new ExpressionResult($scope, $hasYield, $throwPoints); - }, - true - ); - $scope = $result->getScope(); - $hasYield = $result->hasYield(); - $throwPoints = $result->getThrowPoints(); + return new ExpressionResult($scope, $hasYield, $throwPoints); + }, + true, + ); + $scope = $result->getScope(); + $hasYield = $result->hasYield(); + $throwPoints = $result->getThrowPoints(); + $vars = $this->getAssignedVariables($expr->var); + if (count($vars) > 0) { $varChangedScope = false; - if ($expr->var instanceof Variable && is_string($expr->var->name)) { - $scope = $this->processVarAnnotation($scope, [$expr->var->name], $expr, $varChangedScope); - } - + $scope = $this->processVarAnnotation($scope, $vars, $expr, $varChangedScope); if (!$varChangedScope) { $scope = $this->processStmtVarAnnotation($scope, new Node\Stmt\Expression($expr, [ 'comments' => $expr->getAttribute('comments'), ]), null); } - } else { - $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); - $hasYield = $result->hasYield(); - $throwPoints = $result->getThrowPoints(); - $scope = $result->getScope(); - foreach ($expr->var->items as $arrayItem) { - if ($arrayItem === null) { - continue; - } - - $itemScope = $scope; - if ($arrayItem->value instanceof ArrayDimFetch && $arrayItem->value->dim === null) { - $itemScope = $itemScope->enterExpressionAssign($arrayItem->value); - } - $itemScope = $this->lookForEnterVariableAssign($itemScope, $arrayItem->value); - - $itemResult = $this->processExprNode($arrayItem, $itemScope, $nodeCallback, $context->enterDeep()); - $hasYield = $hasYield || $itemResult->hasYield(); - $throwPoints = array_merge($throwPoints, $itemResult->getThrowPoints()); - $scope = $result->getScope(); - } - $scope = $this->lookForArrayDestructuringArray($scope, $expr->var, $scope->getType($expr->expr)); - $vars = $this->getAssignedVariables($expr->var); - - if (count($vars) > 0) { - $varChangedScope = false; - $scope = $this->processVarAnnotation($scope, $vars, $expr, $varChangedScope); - if (!$varChangedScope) { - $scope = $this->processStmtVarAnnotation($scope, new Node\Stmt\Expression($expr, [ - 'comments' => $expr->getAttribute('comments'), - ]), null); - } - } } } elseif ($expr instanceof Expr\AssignOp) { $result = $this->processAssignVar( @@ -1791,10 +1752,8 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression $expr, $nodeCallback, $context, - function (MutatingScope $scope) use ($expr, $nodeCallback, $context): ExpressionResult { - return $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); - }, - $expr instanceof Expr\AssignOp\Coalesce + fn (MutatingScope $scope): ExpressionResult => $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()), + $expr instanceof Expr\AssignOp\Coalesce, ); $scope = $result->getScope(); $hasYield = $result->hasYield(); @@ -1804,6 +1763,14 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression $functionReflection = null; $throwPoints = []; if ($expr->name instanceof Expr) { + $nameType = $scope->getType($expr->name); + if ($nameType->isCallable()->yes()) { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $expr->getArgs(), + $nameType->getCallableParametersAcceptors($scope), + ); + } $nameResult = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $nameResult->getThrowPoints(); $scope = $nameResult->getScope(); @@ -1811,11 +1778,11 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope); $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, - $expr->args, - $functionReflection->getVariants() + $expr->getArgs(), + $functionReflection->getVariants(), ); } - $result = $this->processArgs($functionReflection, $parametersAcceptor, $expr->args, $scope, $nodeCallback, $context); + $result = $this->processArgs($functionReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); @@ -1842,13 +1809,11 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression if ( isset($functionReflection) && in_array($functionReflection->getName(), ['array_pop', 'array_shift'], true) - && count($expr->args) >= 1 + && count($expr->getArgs()) >= 1 ) { - $arrayArg = $expr->args[0]->value; + $arrayArg = $expr->getArgs()[0]->value; $constantArrays = TypeUtils::getConstantArrays($scope->getType($arrayArg)); - $scope = $scope->invalidateExpression($arrayArg) - ->invalidateExpression(new FuncCall(new Name\FullyQualified('count'), [$expr->args[0]])) - ->invalidateExpression(new FuncCall(new Name('count'), [$expr->args[0]])); + $scope = $scope->invalidateExpression($arrayArg); if (count($constantArrays) > 0) { $resultArrayTypes = []; @@ -1862,7 +1827,7 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression $scope = $scope->specifyExpressionType( $arrayArg, - TypeCombinator::union(...$resultArrayTypes) + TypeCombinator::union(...$resultArrayTypes), ); } else { $arrays = TypeUtils::getAnyArrays($scope->getType($arrayArg)); @@ -1875,10 +1840,10 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression if ( isset($functionReflection) && in_array($functionReflection->getName(), ['array_push', 'array_unshift'], true) - && count($expr->args) >= 2 + && count($expr->getArgs()) >= 2 ) { $argumentTypes = []; - foreach (array_slice($expr->args, 1) as $callArg) { + foreach (array_slice($expr->getArgs(), 1) as $callArg) { $callArgType = $scope->getType($callArg->value); if ($callArg->unpack) { $iterableValueType = $callArgType->getIterableValueType(); @@ -1895,7 +1860,7 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression $argumentTypes[] = $callArgType; } - $arrayArg = $expr->args[0]->value; + $arrayArg = $expr->getArgs()[0]->value; $originalArrayType = $scope->getType($arrayArg); $constantArrays = TypeUtils::getConstantArrays($originalArrayType); if ( @@ -1931,7 +1896,7 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression $scope = $scope->invalidateExpression($arrayArg)->specifyExpressionType( $arrayArg, - TypeCombinator::union(...$arrayTypes) + TypeCombinator::union(...$arrayTypes), ); } } @@ -1943,6 +1908,23 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression $scope = $scope->assignVariable('http_response_header', new ArrayType(new IntegerType(), new StringType())); } + if ( + isset($functionReflection) + && $functionReflection->getName() === 'array_splice' + && count($expr->getArgs()) >= 1 + ) { + $arrayArg = $expr->getArgs()[0]->value; + $arrayArgType = $scope->getType($arrayArg); + $valueType = $arrayArgType->getIterableValueType(); + if (count($expr->getArgs()) >= 4) { + $valueType = TypeCombinator::union($valueType, $scope->getType($expr->getArgs()[3]->value)->getIterableValueType()); + } + $scope = $scope->invalidateExpression($arrayArg)->specifyExpressionType( + $arrayArg, + new ArrayType($arrayArgType->getIterableKeyType(), $valueType), + ); + } + if (isset($functionReflection) && $functionReflection->getName() === 'extract') { $scope = $scope->afterExtractCall(); } @@ -1952,7 +1934,7 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression } if (isset($functionReflection) && $functionReflection->hasSideEffects()->yes()) { - foreach ($expr->args as $arg) { + foreach ($expr->getArgs() as $arg) { $scope = $scope->invalidateExpression($arg->value, true); } } @@ -1963,9 +1945,9 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression ($expr->var instanceof Expr\Closure || $expr->var instanceof Expr\ArrowFunction) && $expr->name instanceof Node\Identifier && strtolower($expr->name->name) === 'call' - && isset($expr->args[0]) + && isset($expr->getArgs()[0]) ) { - $closureCallScope = $scope->enterClosureCall($scope->getType($expr->args[0]->value)); + $closureCallScope = $scope->enterClosureCall($scope->getType($expr->getArgs()[0]->value)); } $result = $this->processExprNode($expr->var, $closureCallScope ?? $scope, $nodeCallback, $context->enterDeep()); @@ -1988,27 +1970,27 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression if ($methodReflection !== null) { $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, - $expr->args, - $methodReflection->getVariants() + $expr->getArgs(), + $methodReflection->getVariants(), ); - $methodThrowPoint = $this->getMethodThrowPoint($methodReflection, $expr, $scope); + $methodThrowPoint = $this->getMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope); if ($methodThrowPoint !== null) { $throwPoints[] = $methodThrowPoint; } - } else { - $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); } } - $result = $this->processArgs($methodReflection, $parametersAcceptor, $expr->args, $scope, $nodeCallback, $context); + $result = $this->processArgs($methodReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context); $scope = $result->getScope(); if ($methodReflection !== null) { $hasSideEffects = $methodReflection->hasSideEffects(); if ($hasSideEffects->yes()) { $scope = $scope->invalidateExpression($expr->var, true); - foreach ($expr->args as $arg) { + foreach ($expr->getArgs() as $arg) { $scope = $scope->invalidateExpression($arg->value, true); } } + } else { + $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); } $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); @@ -2021,12 +2003,8 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression $scope, $exprResult->hasYield(), $exprResult->getThrowPoints(), - static function () use ($scope, $expr): MutatingScope { - return $scope->filterByTruthyValue($expr); - }, - static function () use ($scope, $expr): MutatingScope { - return $scope->filterByFalseyValue($expr); - } + static fn (): MutatingScope => $scope->filterByTruthyValue($expr), + static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } elseif ($expr instanceof StaticCall) { $hasYield = false; @@ -2072,8 +2050,8 @@ static function () use ($scope, $expr): MutatingScope { $methodReflection = $classReflection->getMethod($methodName, $scope); $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, - $expr->args, - $methodReflection->getVariants() + $expr->getArgs(), + $methodReflection->getVariants(), ); $methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $expr, $scope); if ($methodThrowPoint !== null) { @@ -2084,8 +2062,8 @@ static function () use ($scope, $expr): MutatingScope { && strtolower($methodName) === 'bind' ) { $thisType = null; - if (isset($expr->args[1])) { - $argType = $scope->getType($expr->args[1]->value); + if (isset($expr->getArgs()[1])) { + $argType = $scope->getType($expr->getArgs()[1]->value); if ($argType instanceof NullType) { $thisType = null; } else { @@ -2093,25 +2071,23 @@ static function () use ($scope, $expr): MutatingScope { } } $scopeClass = 'static'; - if (isset($expr->args[2])) { - $argValue = $expr->args[2]->value; + if (isset($expr->getArgs()[2])) { + $argValue = $expr->getArgs()[2]->value; $argValueType = $scope->getType($argValue); $directClassNames = TypeUtils::getDirectClassNames($argValueType); if (count($directClassNames) === 1) { $scopeClass = $directClassNames[0]; $thisType = new ObjectType($scopeClass); - } elseif ( - $argValue instanceof Expr\ClassConstFetch - && $argValue->name instanceof Node\Identifier - && strtolower($argValue->name->name) === 'class' - && $argValue->class instanceof Name - ) { - $scopeClass = $scope->resolveName($argValue->class); - $thisType = new ObjectType($scopeClass); } elseif ($argValueType instanceof ConstantStringType) { $scopeClass = $argValueType->getValue(); $thisType = new ObjectType($scopeClass); + } elseif ( + $argValueType instanceof GenericClassStringType + && $argValueType->getGenericType() instanceof TypeWithClassName + ) { + $scopeClass = $argValueType->getGenericType()->getClassName(); + $thisType = $argValueType->getGenericType(); } } $closureBindScope = $scope->enterClosureBind($thisType, $scopeClass); @@ -2123,7 +2099,7 @@ static function () use ($scope, $expr): MutatingScope { $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); } } - $result = $this->processArgs($methodReflection, $parametersAcceptor, $expr->args, $scope, $nodeCallback, $context, $closureBindScope ?? null); + $result = $this->processArgs($methodReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context, $closureBindScope ?? null); $scope = $result->getScope(); $scopeFunction = $scope->getFunction(); if ( @@ -2143,7 +2119,7 @@ static function () use ($scope, $expr): MutatingScope { if ($methodReflection !== null) { if ($methodReflection->hasSideEffects()->yes()) { - foreach ($expr->args as $arg) { + foreach ($expr->getArgs() as $arg) { $scope = $scope->invalidateExpression($arg->value, true); } } @@ -2170,12 +2146,8 @@ static function () use ($scope, $expr): MutatingScope { $scope, $exprResult->hasYield(), $exprResult->getThrowPoints(), - static function () use ($scope, $expr): MutatingScope { - return $scope->filterByTruthyValue($expr); - }, - static function () use ($scope, $expr): MutatingScope { - return $scope->filterByFalseyValue($expr); - } + static fn (): MutatingScope => $scope->filterByTruthyValue($expr), + static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } elseif ($expr instanceof StaticPropertyFetch) { $hasYield = false; @@ -2276,12 +2248,8 @@ static function () use ($scope, $expr): MutatingScope { $leftMergedWithRightScope, $leftResult->hasYield() || $rightResult->hasYield(), array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), - static function () use ($expr, $rightResult): MutatingScope { - return $rightResult->getScope()->filterByTruthyValue($expr); - }, - static function () use ($leftMergedWithRightScope, $expr): MutatingScope { - return $leftMergedWithRightScope->filterByFalseyValue($expr); - } + static fn (): MutatingScope => $rightResult->getScope()->filterByTruthyValue($expr), + static fn (): MutatingScope => $leftMergedWithRightScope->filterByFalseyValue($expr), ); } elseif ($expr instanceof BooleanOr || $expr instanceof BinaryOp\LogicalOr) { $leftResult = $this->processExprNode($expr->left, $scope, $nodeCallback, $context->enterDeep()); @@ -2294,18 +2262,20 @@ static function () use ($leftMergedWithRightScope, $expr): MutatingScope { $leftMergedWithRightScope, $leftResult->hasYield() || $rightResult->hasYield(), array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()), - static function () use ($leftMergedWithRightScope, $expr): MutatingScope { - return $leftMergedWithRightScope->filterByTruthyValue($expr); - }, - static function () use ($expr, $rightResult): MutatingScope { - return $rightResult->getScope()->filterByFalseyValue($expr); - } + static fn (): MutatingScope => $leftMergedWithRightScope->filterByTruthyValue($expr), + static fn (): MutatingScope => $rightResult->getScope()->filterByFalseyValue($expr), ); } elseif ($expr instanceof Coalesce) { $nonNullabilityResult = $this->ensureNonNullability($scope, $expr->left, false); - if ($expr->left instanceof PropertyFetch || $expr->left instanceof StaticPropertyFetch || $expr->left instanceof Expr\NullsafePropertyFetch) { - $scope = $nonNullabilityResult->getScope(); + if ($expr->left instanceof PropertyFetch || $expr->left instanceof Expr\NullsafePropertyFetch) { + $scope = $this->lookForEnterVariableAssign($nonNullabilityResult->getScope(), $expr->left->var); + } elseif ($expr->left instanceof StaticPropertyFetch) { + if ($expr->left->class instanceof Expr) { + $scope = $this->lookForEnterVariableAssign($nonNullabilityResult->getScope(), $expr->left->class); + } else { + $scope = $nonNullabilityResult->getScope(); + } } else { $scope = $this->lookForEnterVariableAssign($nonNullabilityResult->getScope(), $expr->left); } @@ -2315,7 +2285,13 @@ static function () use ($expr, $rightResult): MutatingScope { $scope = $result->getScope(); $scope = $this->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions()); - if (!$expr->left instanceof PropertyFetch) { + if ($expr->left instanceof PropertyFetch || $expr->left instanceof Expr\NullsafePropertyFetch) { + $scope = $this->lookForExitVariableAssign($scope, $expr->left->var); + } elseif ($expr->left instanceof StaticPropertyFetch) { + if ($expr->left->class instanceof Expr) { + $scope = $this->lookForExitVariableAssign($scope, $expr->left->class); + } + } else { $scope = $this->lookForExitVariableAssign($scope, $expr->left); } $result = $this->processExprNode($expr->right, $scope, $nodeCallback, $context->enterDeep()); @@ -2341,7 +2317,6 @@ static function () use ($expr, $rightResult): MutatingScope { $expr instanceof Expr\BitwiseNot || $expr instanceof Cast || $expr instanceof Expr\Clone_ - || $expr instanceof Expr\Eval_ || $expr instanceof Expr\Print_ || $expr instanceof Expr\UnaryMinus || $expr instanceof Expr\UnaryPlus @@ -2350,6 +2325,13 @@ static function () use ($expr, $rightResult): MutatingScope { $throwPoints = $result->getThrowPoints(); $hasYield = $result->hasYield(); + $scope = $result->getScope(); + } elseif ($expr instanceof Expr\Eval_) { + $result = $this->processExprNode($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()); @@ -2445,10 +2427,10 @@ static function () use ($expr, $rightResult): MutatingScope { $constructorReflection = $classReflection->getConstructor(); $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, - $expr->args, - $constructorReflection->getVariants() + $expr->getArgs(), + $constructorReflection->getVariants(), ); - $constructorThrowPoint = $this->getConstructorThrowPoint($constructorReflection, $classReflection, $expr, $expr->class, $expr->args, $scope); + $constructorThrowPoint = $this->getConstructorThrowPoint($constructorReflection, $classReflection, $expr, $expr->class, $expr->getArgs(), $scope); if ($constructorThrowPoint !== null) { $throwPoints[] = $constructorThrowPoint; } @@ -2457,7 +2439,7 @@ static function () use ($expr, $rightResult): MutatingScope { $throwPoints[] = ThrowPoint::createImplicit($scope, $expr); } } - $result = $this->processArgs($constructorReflection, $parametersAcceptor, $expr->args, $scope, $nodeCallback, $context); + $result = $this->processArgs($constructorReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context); $scope = $result->getScope(); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); @@ -2471,36 +2453,29 @@ static function () use ($expr, $rightResult): MutatingScope { $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = []; - if ( - $expr->var instanceof Variable - || $expr->var instanceof ArrayDimFetch - || $expr->var instanceof PropertyFetch - || $expr->var instanceof StaticPropertyFetch - ) { - $newExpr = $expr; - if ($expr instanceof Expr\PostInc) { - $newExpr = new Expr\PreInc($expr->var); - } elseif ($expr instanceof Expr\PostDec) { - $newExpr = new Expr\PreDec($expr->var); - } - if (!$scope->getType($expr->var)->equals($scope->getType($newExpr))) { - $scope = $this->processAssignVar( - $scope, - $expr->var, - $newExpr, - static function (): void { - }, - $context, - static function (MutatingScope $scope): ExpressionResult { - return new ExpressionResult($scope, false, []); - }, - false - )->getScope(); - } else { - $scope = $scope->invalidateExpression($expr->var); - } + $newExpr = $expr; + if ($expr instanceof Expr\PostInc) { + $newExpr = new Expr\PreInc($expr->var); + } elseif ($expr instanceof Expr\PostDec) { + $newExpr = new Expr\PreDec($expr->var); } + + $scope = $this->processAssignVar( + $scope, + $expr->var, + $newExpr, + static function (Node $node, Scope $scope) use ($nodeCallback): void { + if (!$node instanceof PropertyAssignNode) { + return; + } + + $nodeCallback($node, $scope); + }, + $context, + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, []), + false, + )->getScope(); } elseif ($expr instanceof Ternary) { $ternaryCondResult = $this->processExprNode($expr->cond, $scope, $nodeCallback, $context->enterDeep()); $throwPoints = $ternaryCondResult->getThrowPoints(); @@ -2523,12 +2498,8 @@ static function (MutatingScope $scope): ExpressionResult { $finalScope, $ternaryCondResult->hasYield(), $throwPoints, - static function () use ($finalScope, $expr): MutatingScope { - return $finalScope->filterByTruthyValue($expr); - }, - static function () use ($finalScope, $expr): MutatingScope { - return $finalScope->filterByFalseyValue($expr); - } + static fn (): MutatingScope => $finalScope->filterByTruthyValue($expr), + static fn (): MutatingScope => $finalScope->filterByFalseyValue($expr), ); } elseif ($expr instanceof Expr\Yield_) { @@ -2566,7 +2537,7 @@ static function () use ($finalScope, $expr): MutatingScope { } if (count($arm->conds) === 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $filteringExpr = null; @@ -2593,7 +2564,7 @@ static function () use ($finalScope, $expr): MutatingScope { $arm->body, $matchScope->filterByTruthyValue($filteringExpr), $nodeCallback, - ExpressionContext::createTopLevel() + ExpressionContext::createTopLevel(), ); $armScope = $armResult->getScope(); $scope = $scope->mergeWith($armScope); @@ -2608,6 +2579,50 @@ static function () use ($finalScope, $expr): MutatingScope { $result = $this->processExprNode($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()); + $scope = $result->getScope(); + $hasYield = $result->hasYield(); + $throwPoints = $result->getThrowPoints(); + } + } elseif ($expr instanceof MethodCallableNode) { + $result = $this->processExprNode($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()); + $scope = $nameResult->getScope(); + $hasYield = $hasYield || $nameResult->hasYield(); + $throwPoints = array_merge($throwPoints, $nameResult->getThrowPoints()); + } + } elseif ($expr instanceof StaticMethodCallableNode) { + $throwPoints = []; + $hasYield = false; + if ($expr->getClass() instanceof Expr) { + $classResult = $this->processExprNode($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()); + $scope = $nameResult->getScope(); + $hasYield = $hasYield || $nameResult->hasYield(); + $throwPoints = array_merge($throwPoints, $nameResult->getThrowPoints()); + } + } elseif ($expr instanceof InstantiationCallableNode) { + $throwPoints = []; + $hasYield = false; + if ($expr->getClass() instanceof Expr) { + $classResult = $this->processExprNode($expr->getClass(), $scope, $nodeCallback, ExpressionContext::createDeep()); + $scope = $classResult->getScope(); + $hasYield = $classResult->hasYield(); + $throwPoints = $classResult->getThrowPoints(); + } } else { $hasYield = false; $throwPoints = []; @@ -2617,12 +2632,8 @@ static function () use ($finalScope, $expr): MutatingScope { $scope, $hasYield, $throwPoints, - static function () use ($scope, $expr): MutatingScope { - return $scope->filterByTruthyValue($expr); - }, - static function () use ($scope, $expr): MutatingScope { - return $scope->filterByFalseyValue($expr); - } + static fn (): MutatingScope => $scope->filterByTruthyValue($expr), + static fn (): MutatingScope => $scope->filterByFalseyValue($expr), ); } @@ -2630,7 +2641,7 @@ private function getFunctionThrowPoint( FunctionReflection $functionReflection, ?ParametersAcceptor $parametersAcceptor, FuncCall $funcCall, - MutatingScope $scope + MutatingScope $scope, ): ?ThrowPoint { foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicFunctionThrowTypeExtensions() as $extension) { @@ -2646,8 +2657,15 @@ private function getFunctionThrowPoint( return ThrowPoint::createExplicit($scope, $throwType, $funcCall, false); } - if ($functionReflection->getThrowType() !== null) { - $throwType = $functionReflection->getThrowType(); + $throwType = $functionReflection->getThrowType(); + if ($throwType === null && $parametersAcceptor !== null) { + $returnType = $parametersAcceptor->getReturnType(); + if ($returnType instanceof NeverType && $returnType->isExplicit()) { + $throwType = new ObjectType(Throwable::class); + } + } + + if ($throwType !== null) { if (!$throwType instanceof VoidType) { return ThrowPoint::createExplicit($scope, $throwType, $funcCall, true); } @@ -2667,10 +2685,10 @@ private function getFunctionThrowPoint( !$functionReflection->isBuiltin() || $requiredParameters === null || $requiredParameters > 0 - || count($funcCall->args) > 0 + || count($funcCall->getArgs()) > 0 ) { $functionReturnedType = $scope->getType($funcCall); - if (!(new ObjectType(\Throwable::class))->isSuperTypeOf($functionReturnedType)->yes()) { + if (!(new ObjectType(Throwable::class))->isSuperTypeOf($functionReturnedType)->yes()) { return ThrowPoint::createImplicit($scope, $funcCall); } } @@ -2679,7 +2697,7 @@ private function getFunctionThrowPoint( return null; } - private function getMethodThrowPoint(MethodReflection $methodReflection, MethodCall $methodCall, MutatingScope $scope): ?ThrowPoint + private function getMethodThrowPoint(MethodReflection $methodReflection, ParametersAcceptor $parametersAcceptor, MethodCall $methodCall, MutatingScope $scope): ?ThrowPoint { foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicMethodThrowTypeExtensions() as $extension) { if (!$extension->isMethodSupported($methodReflection)) { @@ -2694,14 +2712,21 @@ private function getMethodThrowPoint(MethodReflection $methodReflection, MethodC return ThrowPoint::createExplicit($scope, $throwType, $methodCall, false); } - if ($methodReflection->getThrowType() !== null) { - $throwType = $methodReflection->getThrowType(); + $throwType = $methodReflection->getThrowType(); + if ($throwType === null) { + $returnType = $parametersAcceptor->getReturnType(); + if ($returnType instanceof NeverType && $returnType->isExplicit()) { + $throwType = new ObjectType(Throwable::class); + } + } + + if ($throwType !== null) { if (!$throwType instanceof VoidType) { return ThrowPoint::createExplicit($scope, $throwType, $methodCall, true); } } elseif ($this->implicitThrows) { $methodReturnedType = $scope->getType($methodCall); - if (!(new ObjectType(\Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) { + if (!(new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) { return ThrowPoint::createImplicit($scope, $methodCall); } } @@ -2734,7 +2759,7 @@ private function getConstructorThrowPoint(MethodReflection $constructorReflectio return ThrowPoint::createExplicit($scope, $throwType, $new, true); } } elseif ($this->implicitThrows) { - if ($classReflection->getName() !== \Throwable::class && !$classReflection->isSubclassOf(\Throwable::class)) { + if ($classReflection->getName() !== Throwable::class && !$classReflection->isSubclassOf(Throwable::class)) { return ThrowPoint::createImplicit($scope, $methodCall); } } @@ -2764,7 +2789,7 @@ private function getStaticMethodThrowPoint(MethodReflection $methodReflection, S } } elseif ($this->implicitThrows) { $methodReturnedType = $scope->getType($methodCall); - if (!(new ObjectType(\Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) { + if (!(new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) { return ThrowPoint::createImplicit($scope, $methodCall); } } @@ -2773,7 +2798,6 @@ private function getStaticMethodThrowPoint(MethodReflection $methodReflection, S } /** - * @param Expr $expr * @return string[] */ private function getAssignedVariables(Expr $expr): array @@ -2799,20 +2823,21 @@ private function getAssignedVariables(Expr $expr): array return $names; } + if ($expr instanceof ArrayDimFetch) { + return $this->getAssignedVariables($expr->var); + } + return []; } /** - * @param callable(\PhpParser\Node $node, Scope $scope): void $nodeCallback - * @param Expr $expr - * @param MutatingScope $scope - * @param ExpressionContext $context + * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function callNodeCallbackWithExpression( callable $nodeCallback, Expr $expr, MutatingScope $scope, - ExpressionContext $context + ExpressionContext $context, ): void { if ($context->isDeep()) { @@ -2822,19 +2847,14 @@ private function callNodeCallbackWithExpression( } /** - * @param \PhpParser\Node\Expr\Closure $expr - * @param \PHPStan\Analyser\MutatingScope $scope - * @param callable(\PhpParser\Node $node, Scope $scope): void $nodeCallback - * @param ExpressionContext $context - * @param Type|null $passedToType - * @return \PHPStan\Analyser\ExpressionResult + * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function processClosureNode( Expr\Closure $expr, MutatingScope $scope, callable $nodeCallback, ExpressionContext $context, - ?Type $passedToType + ?Type $passedToType, ): ExpressionResult { foreach ($expr->params as $param) { @@ -2844,6 +2864,13 @@ private function processClosureNode( $byRefUses = []; if ($passedToType !== null && !$passedToType->isCallable()->no()) { + if ($passedToType instanceof UnionType) { + $passedToType = TypeCombinator::union(...array_filter( + $passedToType->getTypes(), + static fn (Type $type) => $type->isCallable()->yes(), + )); + } + $callableParameters = null; $acceptors = $passedToType->getCallableParametersAcceptors($scope); if (count($acceptors) === 1) { @@ -2896,7 +2923,7 @@ private function processClosureNode( $gatheredReturnStatements = []; $gatheredYieldStatements = []; - $closureStmtsCallback = static function (\PhpParser\Node $node, Scope $scope) use ($nodeCallback, &$gatheredReturnStatements, &$gatheredYieldStatements, &$closureScope): void { + $closureStmtsCallback = static function (Node $node, Scope $scope) use ($nodeCallback, &$gatheredReturnStatements, &$gatheredYieldStatements, &$closureScope): void { $nodeCallback($node, $scope); if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) { return; @@ -2916,7 +2943,7 @@ private function processClosureNode( $expr, $gatheredReturnStatements, $gatheredYieldStatements, - $statementResult + $statementResult, ), $closureScope); return new ExpressionResult($scope, false, []); @@ -2937,6 +2964,9 @@ private function processClosureNode( if ($closureScope->equals($prevScope)) { break; } + if ($count >= self::GENERALIZE_AFTER_ITERATION) { + $closureScope = $prevScope->generalizeWith($closureScope); + } $count++; } while ($count < self::LOOP_SCOPE_ITERATIONS); @@ -2945,26 +2975,21 @@ private function processClosureNode( $expr, $gatheredReturnStatements, $gatheredYieldStatements, - $statementResult + $statementResult, ), $closureScope); return new ExpressionResult($scope->processClosureScope($closureScope, null, $byRefUses), false, []); } /** - * @param \PhpParser\Node\Expr\ArrowFunction $expr - * @param \PHPStan\Analyser\MutatingScope $scope - * @param callable(\PhpParser\Node $node, Scope $scope): void $nodeCallback - * @param ExpressionContext $context - * @param Type|null $passedToType - * @return \PHPStan\Analyser\ExpressionResult + * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function processArrowFunctionNode( Expr\ArrowFunction $expr, MutatingScope $scope, callable $nodeCallback, ExpressionContext $context, - ?Type $passedToType + ?Type $passedToType, ): ExpressionResult { foreach ($expr->params as $param) { @@ -2975,6 +3000,13 @@ private function processArrowFunctionNode( } if ($passedToType !== null && !$passedToType->isCallable()->no()) { + if ($passedToType instanceof UnionType) { + $passedToType = TypeCombinator::union(...array_filter( + $passedToType->getTypes(), + static fn (Type $type) => $type->isCallable()->yes(), + )); + } + $callableParameters = null; $acceptors = $passedToType->getCallableParametersAcceptors($scope); if (count($acceptors) === 1) { @@ -2991,66 +3023,19 @@ private function processArrowFunctionNode( return new ExpressionResult($scope, false, []); } - private function lookForArrayDestructuringArray(MutatingScope $scope, Expr $expr, Type $valueType): MutatingScope - { - if ($expr instanceof Array_ || $expr instanceof List_) { - foreach ($expr->items as $key => $item) { - /** @var \PhpParser\Node\Expr\ArrayItem|null $itemValue */ - $itemValue = $item; - if ($itemValue === null) { - continue; - } - - $keyType = $itemValue->key === null ? new ConstantIntegerType($key) : $scope->getType($itemValue->key); - $scope = $this->specifyItemFromArrayDestructuring($scope, $itemValue, $valueType, $keyType); - } - } elseif ($expr instanceof Variable && is_string($expr->name)) { - $scope = $scope->assignVariable($expr->name, new MixedType()); - } elseif ($expr instanceof ArrayDimFetch && $expr->var instanceof Variable && is_string($expr->var->name)) { - $scope = $scope->assignVariable($expr->var->name, new MixedType()); - } - - return $scope; - } - - private function specifyItemFromArrayDestructuring(MutatingScope $scope, ArrayItem $arrayItem, Type $valueType, Type $keyType): MutatingScope - { - $type = $valueType->getOffsetValueType($keyType); - - $itemNode = $arrayItem->value; - if ($itemNode instanceof Variable && is_string($itemNode->name)) { - $scope = $scope->assignVariable($itemNode->name, $type); - } elseif ($itemNode instanceof ArrayDimFetch && $itemNode->var instanceof Variable && is_string($itemNode->var->name)) { - $currentType = $scope->hasVariableType($itemNode->var->name)->no() - ? new ConstantArrayType([], []) - : $scope->getVariableType($itemNode->var->name); - $dimType = null; - if ($itemNode->dim !== null) { - $dimType = $scope->getType($itemNode->dim); - } - $scope = $scope->assignVariable($itemNode->var->name, $currentType->setOffsetValueType($dimType, $type)); - } else { - $scope = $this->lookForArrayDestructuringArray($scope, $itemNode, $type); - } - - return $scope; - } - /** - * @param \PhpParser\Node\Param $param - * @param \PHPStan\Analyser\MutatingScope $scope - * @param callable(\PhpParser\Node $node, Scope $scope): void $nodeCallback + * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function processParamNode( Node\Param $param, MutatingScope $scope, - callable $nodeCallback + callable $nodeCallback, ): void { foreach ($param->attrGroups as $attrGroup) { foreach ($attrGroup->attrs as $attr) { foreach ($attr->args as $arg) { - $nodeCallback($arg->value, $scope); + $this->processExprNode($arg->value, $scope, $nodeCallback, ExpressionContext::createDeep()); } } } @@ -3066,14 +3051,9 @@ private function processParamNode( } /** - * @param \PHPStan\Reflection\MethodReflection|\PHPStan\Reflection\FunctionReflection|null $calleeReflection - * @param ParametersAcceptor|null $parametersAcceptor - * @param \PhpParser\Node\Arg[] $args - * @param \PHPStan\Analyser\MutatingScope $scope - * @param callable(\PhpParser\Node $node, Scope $scope): void $nodeCallback - * @param ExpressionContext $context - * @param \PHPStan\Analyser\MutatingScope|null $closureBindScope - * @return \PHPStan\Analyser\ExpressionResult + * @param MethodReflection|FunctionReflection|null $calleeReflection + * @param Node\Arg[] $args + * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function processArgs( $calleeReflection, @@ -3082,7 +3062,7 @@ private function processArgs( MutatingScope $scope, callable $nodeCallback, ExpressionContext $context, - ?MutatingScope $closureBindScope = null + ?MutatingScope $closureBindScope = null, ): ExpressionResult { if ($parametersAcceptor !== null) { @@ -3114,47 +3094,6 @@ private function processArgs( $scope = $scope->assignVariable($argValue->name, new MixedType()); } } - - if ($calleeReflection instanceof FunctionReflection) { - if ( - $i === 0 - && $calleeReflection->getName() === 'array_map' - && isset($args[1]) - ) { - $parameterType = new CallableType([ - new DummyParameter('item', $scope->getType($args[1]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null), - ], new MixedType(), false); - } - - if ( - $i === 1 - && $calleeReflection->getName() === 'array_filter' - && isset($args[0]) - ) { - if (isset($args[2])) { - $mode = $scope->getType($args[2]->value); - if ($mode instanceof ConstantIntegerType) { - if ($mode->getValue() === ARRAY_FILTER_USE_KEY) { - $arrayFilterParameters = [ - new DummyParameter('key', $scope->getType($args[0]->value)->getIterableKeyType(), false, PassedByReference::createNo(), false, null), - ]; - } elseif ($mode->getValue() === ARRAY_FILTER_USE_BOTH) { - $arrayFilterParameters = [ - new DummyParameter('item', $scope->getType($args[0]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null), - new DummyParameter('key', $scope->getType($args[0]->value)->getIterableKeyType(), false, PassedByReference::createNo(), false, null), - ]; - } - } - } - $parameterType = new CallableType( - $arrayFilterParameters ?? [ - new DummyParameter('item', $scope->getType($args[0]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null), - ], - new MixedType(), - false - ); - } - } } $originalScope = $scope; @@ -3190,14 +3129,8 @@ private function processArgs( } /** - * @param \PHPStan\Analyser\MutatingScope $scope - * @param \PhpParser\Node\Expr $var - * @param \PhpParser\Node\Expr $assignedExpr - * @param callable(\PhpParser\Node $node, Scope $scope): void $nodeCallback - * @param ExpressionContext $context - * @param \Closure(MutatingScope $scope): ExpressionResult $processExprCallback - * @param bool $enterExpressionAssign - * @return ExpressionResult + * @param callable(Node $node, Scope $scope): void $nodeCallback + * @param Closure(MutatingScope $scope): ExpressionResult $processExprCallback */ private function processAssignVar( MutatingScope $scope, @@ -3205,13 +3138,14 @@ private function processAssignVar( Expr $assignedExpr, callable $nodeCallback, ExpressionContext $context, - \Closure $processExprCallback, - bool $enterExpressionAssign + Closure $processExprCallback, + bool $enterExpressionAssign, ): ExpressionResult { $nodeCallback($var, $enterExpressionAssign ? $scope->enterExpressionAssign($var) : $scope); $hasYield = false; $throwPoints = []; + $isAssignOp = $assignedExpr instanceof Expr\AssignOp && !$enterExpressionAssign; if ($var instanceof Variable && is_string($var->name)) { $result = $processExprCallback($scope); $hasYield = $result->hasYield(); @@ -3238,20 +3172,30 @@ private function processAssignVar( } elseif ($var instanceof ArrayDimFetch) { $dimExprStack = []; $originalVar = $var; + $assignedPropertyExpr = $assignedExpr; while ($var instanceof ArrayDimFetch) { + $varForSetOffsetValue = $var->var; + if ($varForSetOffsetValue instanceof PropertyFetch || $varForSetOffsetValue instanceof StaticPropertyFetch) { + $varForSetOffsetValue = new OriginalPropertyTypeExpr($varForSetOffsetValue); + } + $assignedPropertyExpr = new SetOffsetValueTypeExpr( + $varForSetOffsetValue, + $var->dim, + $assignedPropertyExpr, + ); $dimExprStack[] = $var->dim; $var = $var->var; } // 1. eval root expr - if ($enterExpressionAssign && $var instanceof Variable) { + if ($enterExpressionAssign) { $scope = $scope->enterExpressionAssign($var); } $result = $this->processExprNode($var, $scope, $nodeCallback, $context->enterDeep()); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $scope = $result->getScope(); - if ($enterExpressionAssign && $var instanceof Variable) { + if ($enterExpressionAssign) { $scope = $scope->exitExpressionAssign($var); } @@ -3288,39 +3232,43 @@ private function processAssignVar( $scope = $result->getScope(); $varType = $scope->getType($var); - if (!(new ObjectType(\ArrayAccess::class))->isSuperTypeOf($varType)->yes()) { - // 4. compose types - if ($varType instanceof ErrorType) { - $varType = new ConstantArrayType([], []); - } - $offsetValueType = $varType; - $offsetValueTypeStack = [$offsetValueType]; - foreach (array_slice($offsetTypes, 0, -1) as $offsetType) { - if ($offsetType === null) { - $offsetValueType = new ConstantArrayType([], []); - } else { - $offsetValueType = $offsetValueType->getOffsetValueType($offsetType); - if ($offsetValueType instanceof ErrorType) { - $offsetValueType = new ConstantArrayType([], []); - } - } + // 4. compose types + if ($varType instanceof ErrorType) { + $varType = new ConstantArrayType([], []); + } + $offsetValueType = $varType; + $offsetValueTypeStack = [$offsetValueType]; + foreach (array_slice($offsetTypes, 0, -1) as $offsetType) { + if ($offsetType === null) { + $offsetValueType = new ConstantArrayType([], []); - $offsetValueTypeStack[] = $offsetValueType; + } else { + $offsetValueType = $offsetValueType->getOffsetValueType($offsetType); + if ($offsetValueType instanceof ErrorType) { + $offsetValueType = new ConstantArrayType([], []); + } } - foreach (array_reverse($offsetTypes) as $i => $offsetType) { - /** @var Type $offsetValueType */ - $offsetValueType = array_pop($offsetValueTypeStack); - $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $i === 0); - } + $offsetValueTypeStack[] = $offsetValueType; + } + foreach (array_reverse($offsetTypes) as $i => $offsetType) { + /** @var Type $offsetValueType */ + $offsetValueType = array_pop($offsetValueTypeStack); + $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $i === 0); + } + + if (!(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->yes()) { if ($var instanceof Variable && is_string($var->name)) { $scope = $scope->assignVariable($var->name, $valueToWrite); } else { + if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { + $nodeCallback(new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope); + } $scope = $scope->assignExpression( $var, - $valueToWrite + $valueToWrite, ); } @@ -3329,10 +3277,24 @@ private function processAssignVar( if (!$originalValueToWrite->isSuperTypeOf($currentVarType)->yes()) { $scope = $scope->assignExpression( $originalVar, - $originalValueToWrite + $originalValueToWrite, ); } } + } else { + if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) { + $nodeCallback(new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope); + } + } + + if (!(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->no()) { + $throwPoints = array_merge($throwPoints, $this->processExprNode( + new MethodCall($var, 'offsetSet'), + $scope, + static function (): void { + }, + $context, + )->getThrowPoints()); } } elseif ($var instanceof PropertyFetch) { $objectResult = $this->processExprNode($var->var, $scope, $nodeCallback, $context); @@ -3358,16 +3320,37 @@ private function processAssignVar( $propertyHolderType = $scope->getType($var->var); if ($propertyName !== null && $propertyHolderType->hasProperty($propertyName)->yes()) { $propertyReflection = $propertyHolderType->getProperty($propertyName, $scope); + $assignedExprType = $scope->getType($assignedExpr); + $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope); if ($propertyReflection->canChangeTypeAfterAssignment()) { - $scope = $scope->assignExpression($var, $scope->getType($assignedExpr)); + $scope = $scope->assignExpression($var, $assignedExprType); + } + $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); } } else { // fallback - $scope = $scope->assignExpression($var, $scope->getType($assignedExpr)); + $assignedExprType = $scope->getType($assignedExpr); + $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope); + $scope = $scope->assignExpression($var, $assignedExprType); + // simulate dynamic property assign by __set to get throw points + if (!$propertyHolderType->hasMethod('__set')->no()) { + $throwPoints = array_merge($throwPoints, $this->processExprNode( + new MethodCall($var->var, '__set'), + $scope, + static function (): void { + }, + $context, + )->getThrowPoints()); + } } } elseif ($var instanceof Expr\StaticPropertyFetch) { - if ($var->class instanceof \PhpParser\Node\Name) { + if ($var->class instanceof Node\Name) { $propertyHolderType = $scope->resolveTypeByName($var->class); } else { $this->processExprNode($var->class, $scope, $nodeCallback, $context); @@ -3393,12 +3376,53 @@ private function processAssignVar( if ($propertyName !== null) { $propertyReflection = $scope->getPropertyReflection($propertyHolderType, $propertyName); + $assignedExprType = $scope->getType($assignedExpr); + $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope); if ($propertyReflection !== null && $propertyReflection->canChangeTypeAfterAssignment()) { - $scope = $scope->assignExpression($var, $scope->getType($assignedExpr)); + $scope = $scope->assignExpression($var, $assignedExprType); } } else { // fallback - $scope = $scope->assignExpression($var, $scope->getType($assignedExpr)); + $assignedExprType = $scope->getType($assignedExpr); + $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope); + $scope = $scope->assignExpression($var, $assignedExprType); + } + } elseif ($var instanceof List_ || $var instanceof Array_) { + $result = $processExprCallback($scope); + $hasYield = $result->hasYield(); + $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $scope = $result->getScope(); + foreach ($var->items as $i => $arrayItem) { + if ($arrayItem === null) { + continue; + } + + $itemScope = $scope; + if ($arrayItem->value instanceof ArrayDimFetch && $arrayItem->value->dim === null) { + $itemScope = $itemScope->enterExpressionAssign($arrayItem->value); + } + $itemScope = $this->lookForEnterVariableAssign($itemScope, $arrayItem->value); + $itemResult = $this->processExprNode($arrayItem, $itemScope, $nodeCallback, $context->enterDeep()); + $hasYield = $hasYield || $itemResult->hasYield(); + $throwPoints = array_merge($throwPoints, $itemResult->getThrowPoints()); + + if ($arrayItem->key === null) { + $dimExpr = new Node\Scalar\LNumber($i); + } else { + $dimExpr = $arrayItem->key; + } + $result = $this->processAssignVar( + $scope, + $arrayItem->value, + new GetOffsetValueTypeExpr($assignedExpr, $dimExpr), + $nodeCallback, + $context, + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, []), + $enterExpressionAssign, + ); + $scope = $result->getScope(); + $hasYield = $hasYield || $result->hasYield(); + $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); } } @@ -3415,11 +3439,7 @@ private function unwrapAssign(Expr $expr): Expr } /** - * @param Scope $scope - * @param string $variableName * @param array $conditionalExpressions - * @param SpecifiedTypes $specifiedTypes - * @param Type $variableType * @return array */ private function processSureTypesForConditionalExpressionsAfterAssign(Scope $scope, string $variableName, array $conditionalExpressions, SpecifiedTypes $specifiedTypes, Type $variableType): array @@ -3439,7 +3459,7 @@ private function processSureTypesForConditionalExpressionsAfterAssign(Scope $sco $conditionalExpressions[$exprString][] = new ConditionalExpressionHolder([ '$' . $variableName => $variableType, ], VariableTypeHolder::createYes( - TypeCombinator::intersect($scope->getType($expr), $exprType) + TypeCombinator::intersect($scope->getType($expr), $exprType), )); } @@ -3447,11 +3467,7 @@ private function processSureTypesForConditionalExpressionsAfterAssign(Scope $sco } /** - * @param Scope $scope - * @param string $variableName * @param array $conditionalExpressions - * @param SpecifiedTypes $specifiedTypes - * @param Type $variableType * @return array */ private function processSureNotTypesForConditionalExpressionsAfterAssign(Scope $scope, string $variableName, array $conditionalExpressions, SpecifiedTypes $specifiedTypes, Type $variableType): array @@ -3471,7 +3487,7 @@ private function processSureNotTypesForConditionalExpressionsAfterAssign(Scope $ $conditionalExpressions[$exprString][] = new ConditionalExpressionHolder([ '$' . $variableName => $variableType, ], VariableTypeHolder::createYes( - TypeCombinator::remove($scope->getType($expr), $exprType) + TypeCombinator::remove($scope->getType($expr), $exprType), )); } @@ -3493,7 +3509,7 @@ private function processStmtVarAnnotation(MutatingScope $scope, Node\Stmt $stmt, $scope->isInClass() ? $scope->getClassReflection()->getName() : null, $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, $function !== null ? $function->getName() : null, - $comment->getText() + $comment->getText(), ); $assignedVariable = null; @@ -3541,11 +3557,7 @@ private function processStmtVarAnnotation(MutatingScope $scope, Node\Stmt $stmt, } /** - * @param MutatingScope $scope * @param array $variableNames - * @param Node $node - * @param bool $changed - * @return MutatingScope */ private function processVarAnnotation(MutatingScope $scope, array $variableNames, Node $node, bool &$changed = false): MutatingScope { @@ -3561,7 +3573,7 @@ private function processVarAnnotation(MutatingScope $scope, array $variableNames $scope->isInClass() ? $scope->getClassReflection()->getName() : null, $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, $function !== null ? $function->getName() : null, - $comment->getText() + $comment->getText(), ); foreach ($resolvedPhpDoc->getVarTags() as $key => $varTag) { $varTags[$key] = $varTag; @@ -3597,25 +3609,41 @@ private function enterForeach(MutatingScope $scope, Foreach_ $stmt): MutatingSco $scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt); } $iterateeType = $scope->getType($stmt->expr); - $vars = []; if ($stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name)) { + $keyVarName = null; + if ($stmt->keyVar !== null + && $stmt->keyVar instanceof Variable + && is_string($stmt->keyVar->name) + ) { + $keyVarName = $stmt->keyVar->name; + } $scope = $scope->enterForeach( $stmt->expr, $stmt->valueVar->name, - $stmt->keyVar !== null - && $stmt->keyVar instanceof Variable - && is_string($stmt->keyVar->name) - ? $stmt->keyVar->name - : null + $keyVarName, ); - $vars[] = $stmt->valueVar->name; - } - - if ( - $stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name) - ) { - $scope = $scope->enterForeachKey($stmt->expr, $stmt->keyVar->name); - $vars[] = $stmt->keyVar->name; + $vars = [$stmt->valueVar->name]; + if ($keyVarName !== null) { + $vars[] = $keyVarName; + } + } else { + $scope = $this->processAssignVar( + $scope, + $stmt->valueVar, + new GetIterableValueTypeExpr($stmt->expr), + static function (): void { + }, + ExpressionContext::createDeep(), + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, []), + true, + )->getScope(); + $vars = $this->getAssignedVariables($stmt->valueVar); + if ( + $stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name) + ) { + $scope = $scope->enterForeachKey($stmt->expr, $stmt->keyVar->name); + $vars[] = $stmt->keyVar->name; + } } if ( @@ -3634,26 +3662,15 @@ private function enterForeach(MutatingScope $scope, Foreach_ $stmt): MutatingSco $scope = $scope->addConditionalExpressions( '$' . $stmt->valueVar->name, - $conditionalHolders + $conditionalHolders, ); } - if ($stmt->valueVar instanceof List_ || $stmt->valueVar instanceof Array_) { - $exprType = $scope->getType($stmt->expr); - $itemType = $exprType->getIterableValueType(); - $scope = $this->lookForArrayDestructuringArray($scope, $stmt->valueVar, $itemType); - $vars = array_merge($vars, $this->getAssignedVariables($stmt->valueVar)); - } - - $scope = $this->processVarAnnotation($scope, $vars, $stmt); - - return $scope; + return $this->processVarAnnotation($scope, $vars, $stmt); } /** - * @param \PhpParser\Node\Stmt\TraitUse $node - * @param MutatingScope $classScope - * @param callable(\PhpParser\Node $node, Scope $scope): void $nodeCallback + * @param callable(Node $node, Scope $scope): void $nodeCallback */ private function processTraitUse(Node\Stmt\TraitUse $node, MutatingScope $classScope, callable $nodeCallback): void { @@ -3664,7 +3681,7 @@ private function processTraitUse(Node\Stmt\TraitUse $node, MutatingScope $classS } $traitReflection = $this->reflectionProvider->getClass($traitName); $traitFileName = $traitReflection->getFileName(); - if ($traitFileName === false) { + if ($traitFileName === null) { continue; // trait from eval or from PHP itself } $fileName = $this->fileHelper->normalizePath($traitFileName); @@ -3672,21 +3689,47 @@ private function processTraitUse(Node\Stmt\TraitUse $node, MutatingScope $classS continue; } $parserNodes = $this->parser->parseFile($fileName); - $this->processNodesForTraitUse($parserNodes, $traitReflection, $classScope, $nodeCallback); + $this->processNodesForTraitUse($parserNodes, $traitReflection, $classScope, $node->adaptations, $nodeCallback); } } /** - * @param \PhpParser\Node[]|\PhpParser\Node|scalar $node - * @param ClassReflection $traitReflection - * @param \PHPStan\Analyser\MutatingScope $scope - * @param callable(\PhpParser\Node $node, Scope $scope): void $nodeCallback + * @param Node[]|Node|scalar $node + * @param Node\Stmt\TraitUseAdaptation[] $adaptations + * @param callable(Node $node, Scope $scope): void $nodeCallback */ - private function processNodesForTraitUse($node, ClassReflection $traitReflection, MutatingScope $scope, callable $nodeCallback): void + private function processNodesForTraitUse($node, ClassReflection $traitReflection, MutatingScope $scope, array $adaptations, callable $nodeCallback): void { if ($node instanceof Node) { if ($node instanceof Node\Stmt\Trait_ && $traitReflection->getName() === (string) $node->namespacedName && $traitReflection->getNativeReflection()->getStartLine() === $node->getStartLine()) { - $this->processStmtNodes($node, $node->stmts, $scope->enterTrait($traitReflection), $nodeCallback); + $methodModifiers = []; + foreach ($adaptations as $adaptation) { + if (!$adaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) { + continue; + } + + if ($adaptation->newModifier === null) { + continue; + } + + $methodModifiers[$adaptation->method->toLowerString()] = $adaptation->newModifier; + } + + $stmts = $node->stmts; + foreach ($stmts as $i => $stmt) { + if (!$stmt instanceof Node\Stmt\ClassMethod) { + continue; + } + $methodName = $stmt->name->toLowerString(); + if (!array_key_exists($methodName, $methodModifiers)) { + continue; + } + + $methodAst = clone $stmt; + $methodAst->flags = ($methodAst->flags & ~ Node\Stmt\Class_::VISIBILITY_MODIFIER_MASK) | $methodModifiers[$methodName]; + $stmts[$i] = $methodAst; + } + $this->processStmtNodes($node, $stmts, $scope->enterTrait($traitReflection), $nodeCallback); return; } if ($node instanceof Node\Stmt\ClassLike) { @@ -3697,18 +3740,16 @@ private function processNodesForTraitUse($node, ClassReflection $traitReflection } foreach ($node->getSubNodeNames() as $subNodeName) { $subNode = $node->{$subNodeName}; - $this->processNodesForTraitUse($subNode, $traitReflection, $scope, $nodeCallback); + $this->processNodesForTraitUse($subNode, $traitReflection, $scope, $adaptations, $nodeCallback); } } elseif (is_array($node)) { foreach ($node as $subNode) { - $this->processNodesForTraitUse($subNode, $traitReflection, $scope, $nodeCallback); + $this->processNodesForTraitUse($subNode, $traitReflection, $scope, $adaptations, $nodeCallback); } } } /** - * @param Scope $scope - * @param Node\FunctionLike $functionLike * @return array{TemplateTypeMap, Type[], ?Type, ?Type, ?string, bool, bool, bool, bool|null} */ public function getPhpDocs(Scope $scope, Node\FunctionLike $functionLike): array @@ -3734,12 +3775,12 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike $functionLike): array if ($functionLike instanceof Node\Stmt\ClassMethod) { if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $functionName = $functionLike->name->name; $positionalParameterNames = array_map(static function (Node\Param $param): string { if (!$param->var instanceof Variable || !is_string($param->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $param->var->name; @@ -3750,7 +3791,7 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike $functionLike): array $scope->getClassReflection(), $trait, $functionLike->name->name, - $positionalParameterNames + $positionalParameterNames, ); if ($functionLike->name->toLowerString() === '__construct') { @@ -3767,7 +3808,7 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike $functionLike): array !$param->var instanceof Variable || !is_string($param->var->name) ) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $paramPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( @@ -3775,7 +3816,7 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike $functionLike): array $class, $trait, '__construct', - $param->getDocComment()->getText() + $param->getDocComment()->getText(), ); $varTags = $paramPhpDoc->getVarTags(); if (isset($varTags[0]) && count($varTags) === 1) { @@ -3799,7 +3840,7 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike $functionLike): array $class, $trait, $functionName, - $docComment + $docComment, ); } diff --git a/src/Analyser/NullsafeOperatorHelper.php b/src/Analyser/NullsafeOperatorHelper.php index 5c4ce696f9..9b34346264 100644 --- a/src/Analyser/NullsafeOperatorHelper.php +++ b/src/Analyser/NullsafeOperatorHelper.php @@ -3,10 +3,25 @@ namespace PHPStan\Analyser; use PhpParser\Node\Expr; +use PHPStan\Type\TypeCombinator; class NullsafeOperatorHelper { + public static function getNullsafeShortcircuitedExprRespectingScope(Scope $scope, Expr $expr): Expr + { + if (!TypeCombinator::containsNull($scope->getType($expr))) { + // We're in most likely in context of a null-safe operator ($scope->moreSpecificType is defined for $expr) + // Modifying the expression would not bring any value or worse ruin the context information + return $expr; + } + + return self::getNullsafeShortcircuitedExpr($expr); + } + + /** + * @internal Use NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope + */ public static function getNullsafeShortcircuitedExpr(Expr $expr): Expr { if ($expr instanceof Expr\NullsafeMethodCall) { @@ -19,7 +34,7 @@ public static function getNullsafeShortcircuitedExpr(Expr $expr): Expr return $expr; } - return new Expr\MethodCall($var, $expr->name, $expr->args); + return new Expr\MethodCall($var, $expr->name, $expr->getArgs()); } if ($expr instanceof Expr\StaticCall && $expr->class instanceof Expr) { @@ -28,7 +43,7 @@ public static function getNullsafeShortcircuitedExpr(Expr $expr): Expr return $expr; } - return new Expr\StaticCall($class, $expr->name, $expr->args); + return new Expr\StaticCall($class, $expr->name, $expr->getArgs()); } if ($expr instanceof Expr\ArrayDimFetch) { diff --git a/src/Analyser/ResultCache/ResultCache.php b/src/Analyser/ResultCache/ResultCache.php index d18cb4a449..c4f5452802 100644 --- a/src/Analyser/ResultCache/ResultCache.php +++ b/src/Analyser/ResultCache/ResultCache.php @@ -8,51 +8,23 @@ class ResultCache { - private bool $fullAnalysis; - - /** @var string[] */ - private array $filesToAnalyse; - - private int $lastFullAnalysisTime; - - /** @var mixed[] */ - private array $meta; - - /** @var array> */ - private array $errors; - - /** @var array> */ - private array $dependencies; - - /** @var array> */ - private array $exportedNodes; - /** * @param string[] $filesToAnalyse - * @param bool $fullAnalysis - * @param int $lastFullAnalysisTime * @param mixed[] $meta * @param array> $errors * @param array> $dependencies * @param array> $exportedNodes */ public function __construct( - array $filesToAnalyse, - bool $fullAnalysis, - int $lastFullAnalysisTime, - array $meta, - array $errors, - array $dependencies, - array $exportedNodes + private array $filesToAnalyse, + private bool $fullAnalysis, + private int $lastFullAnalysisTime, + private array $meta, + private array $errors, + private array $dependencies, + private array $exportedNodes, ) { - $this->filesToAnalyse = $filesToAnalyse; - $this->fullAnalysis = $fullAnalysis; - $this->lastFullAnalysisTime = $lastFullAnalysisTime; - $this->meta = $meta; - $this->errors = $errors; - $this->dependencies = $dependencies; - $this->exportedNodes = $exportedNodes; } /** diff --git a/src/Analyser/ResultCache/ResultCacheClearer.php b/src/Analyser/ResultCache/ResultCacheClearer.php index e6628002bb..d511a3fa92 100644 --- a/src/Analyser/ResultCache/ResultCacheClearer.php +++ b/src/Analyser/ResultCache/ResultCacheClearer.php @@ -3,18 +3,15 @@ namespace PHPStan\Analyser\ResultCache; use Symfony\Component\Finder\Finder; +use function dirname; +use function is_file; +use function unlink; class ResultCacheClearer { - private string $cacheFilePath; - - private string $tempResultCachePath; - - public function __construct(string $cacheFilePath, string $tempResultCachePath) + public function __construct(private string $cacheFilePath, private string $tempResultCachePath) { - $this->cacheFilePath = $cacheFilePath; - $this->tempResultCachePath = $tempResultCachePath; } public function clear(): string diff --git a/src/Analyser/ResultCache/ResultCacheManager.php b/src/Analyser/ResultCache/ResultCacheManager.php index a00e958339..9780ba1cf7 100644 --- a/src/Analyser/ResultCache/ResultCacheManager.php +++ b/src/Analyser/ResultCache/ResultCacheManager.php @@ -2,8 +2,10 @@ namespace PHPStan\Analyser\ResultCache; +use Jean85\PrettyVersions; use Nette\DI\Definitions\Statement; use Nette\Neon\Neon; +use OutOfBoundsException; use PHPStan\Analyser\AnalyserResult; use PHPStan\Analyser\Error; use PHPStan\Command\Output; @@ -13,109 +15,74 @@ use PHPStan\File\FileReader; use PHPStan\File\FileWriter; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\ShouldNotHappenException; +use Throwable; +use function array_diff; use function array_fill_keys; +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 is_array; +use function is_file; +use function is_string; +use function ksort; +use function sha1; +use function sort; +use function sprintf; +use function str_replace; +use function time; +use function unlink; +use function var_export; +use const PHP_VERSION_ID; class ResultCacheManager { private const CACHE_VERSION = 'v9-project-extensions'; - private ExportedNodeFetcher $exportedNodeFetcher; - - private FileFinder $scanFileFinder; - - private ReflectionProvider $reflectionProvider; - - private string $cacheFilePath; - - private string $tempResultCachePath; - - /** @var string[] */ - private array $analysedPaths; - - /** @var string[] */ - private array $composerAutoloaderProjectPaths; - - /** @var string[] */ - private array $stubFiles; - - private string $usedLevel; - - private ?string $cliAutoloadFile; - - /** @var string[] */ - private array $bootstrapFiles; - - /** @var string[] */ - private array $scanFiles; - - /** @var string[] */ - private array $scanDirectories; - /** @var array */ private array $fileHashes = []; - /** @var array */ - private array $fileReplacements = []; - /** @var array */ private array $alreadyProcessed = []; /** - * @param ExportedNodeFetcher $exportedNodeFetcher - * @param FileFinder $scanFileFinder - * @param ReflectionProvider $reflectionProvider - * @param string $cacheFilePath - * @param string $tempResultCachePath * @param string[] $analysedPaths * @param string[] $composerAutoloaderProjectPaths * @param string[] $stubFiles - * @param string $usedLevel - * @param string|null $cliAutoloadFile * @param string[] $bootstrapFiles * @param string[] $scanFiles * @param string[] $scanDirectories * @param array $fileReplacements */ public function __construct( - ExportedNodeFetcher $exportedNodeFetcher, - FileFinder $scanFileFinder, - ReflectionProvider $reflectionProvider, - string $cacheFilePath, - string $tempResultCachePath, - array $analysedPaths, - array $composerAutoloaderProjectPaths, - array $stubFiles, - string $usedLevel, - ?string $cliAutoloadFile, - array $bootstrapFiles, - array $scanFiles, - array $scanDirectories, - array $fileReplacements + private ExportedNodeFetcher $exportedNodeFetcher, + private FileFinder $scanFileFinder, + private ReflectionProvider $reflectionProvider, + private string $cacheFilePath, + private string $tempResultCachePath, + private array $analysedPaths, + private array $composerAutoloaderProjectPaths, + private array $stubFiles, + private string $usedLevel, + private ?string $cliAutoloadFile, + private array $bootstrapFiles, + private array $scanFiles, + private array $scanDirectories, + private array $fileReplacements, + private bool $checkDependenciesOfProjectExtensionFiles, ) { - $this->exportedNodeFetcher = $exportedNodeFetcher; - $this->scanFileFinder = $scanFileFinder; - $this->reflectionProvider = $reflectionProvider; - $this->cacheFilePath = $cacheFilePath; - $this->tempResultCachePath = $tempResultCachePath; - $this->analysedPaths = $analysedPaths; - $this->composerAutoloaderProjectPaths = $composerAutoloaderProjectPaths; - $this->stubFiles = $stubFiles; - $this->usedLevel = $usedLevel; - $this->cliAutoloadFile = $cliAutoloadFile; - $this->bootstrapFiles = $bootstrapFiles; - $this->scanFiles = $scanFiles; - $this->scanDirectories = $scanDirectories; - $this->fileReplacements = $fileReplacements; } /** * @param string[] $allAnalysedFiles * @param mixed[]|null $projectConfigArray - * @param bool $debug - * @return ResultCache */ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?array $projectConfigArray, Output $output, ?string $resultCacheName = null): ResultCache { @@ -149,7 +116,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? try { $data = require $cacheFilePath; - } catch (\Throwable $e) { + } catch (Throwable $e) { if ($output->isDebug()) { $output->writeLineFormatted(sprintf('Result cache not used because an error occurred while loading the cache file: %s', $e->getMessage())); } @@ -212,6 +179,15 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? $filteredErrors = []; $filteredExportedNodes = []; $newFileAppeared = false; + + foreach ($this->stubFiles as $stubFile) { + if (!array_key_exists($stubFile, $errors)) { + continue; + } + + $filteredErrors[$stubFile] = $errors[$stubFile]; + } + foreach ($allAnalysedFiles as $analysedFile) { if (array_key_exists($analysedFile, $errors)) { $filteredErrors[$analysedFile] = $errors[$analysedFile]; @@ -286,7 +262,6 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? /** * @param mixed[] $cachedMeta * @param mixed[] $currentMeta - * @return bool */ private function isMetaDifferent(array $cachedMeta, array $currentMeta): bool { @@ -299,9 +274,7 @@ private function isMetaDifferent(array $cachedMeta, array $currentMeta): bool } /** - * @param string $analysedFile * @param array $cachedFileExportedNodes - * @return bool */ private function exportedNodesChanged(string $analysedFile, array $cachedFileExportedNodes): bool { @@ -324,10 +297,7 @@ private function exportedNodesChanged(string $analysedFile, array $cachedFileExp } /** - * @param AnalyserResult $analyserResult - * @param ResultCache $resultCache * @param bool|string $save - * @return ResultCacheProcessResult */ public function process(AnalyserResult $analyserResult, ResultCache $resultCache, Output $output, bool $onlyFiles, $save): ResultCacheProcessResult { @@ -416,12 +386,11 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache $internalErrors, $dependencies, $exportedNodes, - $analyserResult->hasReachedInternalErrorsCountLimit() + $analyserResult->hasReachedInternalErrorsCountLimit(), ), $saved); } /** - * @param ResultCache $resultCache * @param array> $freshErrorsByFile * @return array> */ @@ -440,7 +409,6 @@ private function mergeErrors(ResultCache $resultCache, array $freshErrorsByFile) } /** - * @param ResultCache $resultCache * @param array>|null $freshDependencies * @return array>|null */ @@ -462,7 +430,7 @@ private function mergeDependencies(ResultCache $resultCache, ?array $freshDepend foreach (array_keys($filesNoOneIsDependingOn) as $file) { if (array_key_exists($file, $cachedDependencies)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $cachedDependencies[$file] = []; @@ -482,7 +450,6 @@ private function mergeDependencies(ResultCache $resultCache, ?array $freshDepend } /** - * @param ResultCache $resultCache * @param array> $freshExportedNodes * @return array> */ @@ -502,8 +469,6 @@ private function mergeExportedNodes(ResultCache $resultCache, array $freshExport } /** - * @param int $lastFullAnalysisTime - * @param string|null $resultCacheName * @param array> $errors * @param array> $dependencies * @param array> $exportedNodes @@ -515,7 +480,7 @@ private function save( array $errors, array $dependencies, array $exportedNodes, - array $meta + array $meta, ): void { $invertedDependencies = []; @@ -535,7 +500,7 @@ private function save( foreach (array_keys($filesNoOneIsDependingOn) as $file) { if (array_key_exists($file, $invertedDependencies)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if (!is_file($file)) { @@ -591,8 +556,8 @@ private function save( var_export($this->getProjectExtensionFiles($projectConfigArray, $dependencies), true), var_export($errors, true), var_export($invertedDependencies, true), - var_export($exportedNodes, true) - ) + var_export($exportedNodes, true), + ), ); } @@ -606,7 +571,10 @@ private function getProjectExtensionFiles(?array $projectConfig, array $dependen $this->alreadyProcessed = []; $projectExtensionFiles = []; if ($projectConfig !== null) { - $services = $projectConfig['services'] ?? []; + $services = array_merge( + $projectConfig['services'] ?? [], + $projectConfig['rules'] ?? [], + ); foreach ($services as $service) { $classes = $this->getClassesFromConfigDefinition($service); if (is_array($service)) { @@ -626,7 +594,7 @@ private function getProjectExtensionFiles(?array $projectConfig, array $dependen $classReflection = $this->reflectionProvider->getClass($class); $fileName = $classReflection->getFileName(); - if ($fileName === false) { + if ($fileName === null) { continue; } @@ -668,7 +636,6 @@ private function getClassesFromConfigDefinition($definition): array } /** - * @param string $fileName * @param array> $dependencies * @return array */ @@ -685,9 +652,12 @@ private function getAllDependencies(string $fileName, array $dependencies): arra $this->alreadyProcessed[$fileName] = true; $files = [$fileName]; - foreach ($dependencies[$fileName] as $fileDep) { - foreach ($this->getAllDependencies($fileDep, $dependencies) as $fileDep2) { - $files[] = $fileDep2; + + if ($this->checkDependenciesOfProjectExtensionFiles) { + foreach ($dependencies[$fileName] as $fileDep) { + foreach ($this->getAllDependencies($fileDep, $dependencies) as $fileDep2) { + $files[] = $fileDep2; + } } } @@ -701,9 +671,7 @@ private function getAllDependencies(string $fileName, array $dependencies): arra */ private function getMeta(array $allAnalysedFiles, ?array $projectConfigArray): array { - $extensions = array_values(array_filter(get_loaded_extensions(), static function (string $extension): bool { - return $extension !== 'xdebug'; - })); + $extensions = array_values(array_filter(get_loaded_extensions(), static fn (string $extension): bool => $extension !== 'xdebug')); sort($extensions); if ($projectConfigArray !== null) { @@ -796,8 +764,8 @@ private function getExecutedFileHashes(): array private function getPhpStanVersion(): string { try { - return \Jean85\PrettyVersions::getVersion('phpstan/phpstan')->getPrettyVersion(); - } catch (\OutOfBoundsException $e) { + return PrettyVersions::getVersion('phpstan/phpstan')->getPrettyVersion(); + } catch (OutOfBoundsException) { return 'Version unknown'; } } diff --git a/src/Analyser/ResultCache/ResultCacheManagerFactory.php b/src/Analyser/ResultCache/ResultCacheManagerFactory.php index c2bdadefaa..333bc6136e 100644 --- a/src/Analyser/ResultCache/ResultCacheManagerFactory.php +++ b/src/Analyser/ResultCache/ResultCacheManagerFactory.php @@ -7,7 +7,6 @@ interface ResultCacheManagerFactory /** * @param array $fileReplacements - * @return ResultCacheManager */ public function create(array $fileReplacements): ResultCacheManager; diff --git a/src/Analyser/ResultCache/ResultCacheProcessResult.php b/src/Analyser/ResultCache/ResultCacheProcessResult.php index b3230ca968..27326f33a0 100644 --- a/src/Analyser/ResultCache/ResultCacheProcessResult.php +++ b/src/Analyser/ResultCache/ResultCacheProcessResult.php @@ -7,14 +7,8 @@ class ResultCacheProcessResult { - private AnalyserResult $analyserResult; - - private bool $saved; - - public function __construct(AnalyserResult $analyserResult, bool $saved) + public function __construct(private AnalyserResult $analyserResult, private bool $saved) { - $this->analyserResult = $analyserResult; - $this->saved = $saved; } public function getAnalyserResult(): AnalyserResult diff --git a/src/Analyser/Scope.php b/src/Analyser/Scope.php index fe9c726a1c..82436e50ac 100644 --- a/src/Analyser/Scope.php +++ b/src/Analyser/Scope.php @@ -2,11 +2,13 @@ namespace PHPStan\Analyser; +use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\Node\Name; use PhpParser\Node\Param; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\PropertyReflection; @@ -29,7 +31,7 @@ public function isInTrait(): bool; public function getTraitReflection(): ?ClassReflection; /** - * @return \PHPStan\Reflection\FunctionReflection|\PHPStan\Reflection\MethodReflection|null + * @return FunctionReflection|MethodReflection|null */ public function getFunction(); @@ -60,7 +62,7 @@ public function isInAnonymousFunction(): bool; public function getAnonymousFunctionReflection(): ?ParametersAcceptor; - public function getAnonymousFunctionReturnType(): ?\PHPStan\Type\Type; + public function getAnonymousFunctionReturnType(): ?Type; public function getType(Expr $node): Type; @@ -69,8 +71,6 @@ public function getType(Expr $node): Type; * Works for function/method parameters only. * * @internal - * @param Expr $expr - * @return Type */ public function getNativeType(Expr $expr): Type; @@ -89,15 +89,14 @@ public function isSpecified(Expr $node): bool; public function isInClassExists(string $className): bool; + public function isInFunctionExists(string $functionName): bool; + public function isInClosureBind(): bool; public function isParameterValueNullable(Param $parameter): bool; /** - * @param \PhpParser\Node\Name|\PhpParser\Node\Identifier|\PhpParser\Node\NullableType|\PhpParser\Node\UnionType|null $type - * @param bool $isNullable - * @param bool $isVariadic - * @return Type + * @param Node\Name|Node\Identifier|Node\ComplexType|null $type */ public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type; diff --git a/src/Analyser/ScopeContext.php b/src/Analyser/ScopeContext.php index ea03d664f9..4118909860 100644 --- a/src/Analyser/ScopeContext.php +++ b/src/Analyser/ScopeContext.php @@ -3,25 +3,17 @@ namespace PHPStan\Analyser; use PHPStan\Reflection\ClassReflection; +use PHPStan\ShouldNotHappenException; class ScopeContext { - private string $file; - - private ?ClassReflection $classReflection; - - private ?ClassReflection $traitReflection; - private function __construct( - string $file, - ?ClassReflection $classReflection, - ?ClassReflection $traitReflection + private string $file, + private ?ClassReflection $classReflection, + private ?ClassReflection $traitReflection, ) { - $this->file = $file; - $this->classReflection = $classReflection; - $this->traitReflection = $traitReflection; } /** @api */ @@ -38,10 +30,10 @@ public function beginFile(): self public function enterClass(ClassReflection $classReflection): self { if ($this->classReflection !== null && !$classReflection->isAnonymous()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ($classReflection->isTrait()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return new self($this->file, $classReflection, null); } @@ -49,10 +41,10 @@ public function enterClass(ClassReflection $classReflection): self public function enterTrait(ClassReflection $traitReflection): self { if ($this->classReflection === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if (!$traitReflection->isTrait()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return new self($this->file, $this->classReflection, $traitReflection); diff --git a/src/Analyser/ScopeFactory.php b/src/Analyser/ScopeFactory.php index b0fd874e7d..1d7db30f3e 100644 --- a/src/Analyser/ScopeFactory.php +++ b/src/Analyser/ScopeFactory.php @@ -12,30 +12,20 @@ interface ScopeFactory /** * @api - * @param \PHPStan\Analyser\ScopeContext $context - * @param bool $declareStrictTypes * @param array $constantTypes - * @param \PHPStan\Reflection\FunctionReflection|\PHPStan\Reflection\MethodReflection|null $function - * @param string|null $namespace - * @param \PHPStan\Analyser\VariableTypeHolder[] $variablesTypes - * @param \PHPStan\Analyser\VariableTypeHolder[] $moreSpecificTypes + * @param VariableTypeHolder[] $variablesTypes + * @param VariableTypeHolder[] $moreSpecificTypes * @param array $conditionalExpressions - * @param string|null $inClosureBindScopeClass - * @param \PHPStan\Reflection\ParametersAcceptor|null $anonymousFunctionReflection - * @param bool $inFirstLevelStatement * @param array $currentlyAssignedExpressions * @param array $nativeExpressionTypes * @param array $inFunctionCallsStack - * @param bool $afterExtractCall - * @param Scope|null $parentScope * - * @return MutatingScope */ public function create( ScopeContext $context, bool $declareStrictTypes = false, array $constantTypes = [], - $function = null, + FunctionReflection|MethodReflection|null $function = null, ?string $namespace = null, array $variablesTypes = [], array $moreSpecificTypes = [], @@ -47,7 +37,7 @@ public function create( array $nativeExpressionTypes = [], array $inFunctionCallsStack = [], bool $afterExtractCall = false, - ?Scope $parentScope = null + ?Scope $parentScope = null, ): MutatingScope; } diff --git a/src/Analyser/SpecifiedTypes.php b/src/Analyser/SpecifiedTypes.php index ec3005c8c1..aefc6214f2 100644 --- a/src/Analyser/SpecifiedTypes.php +++ b/src/Analyser/SpecifiedTypes.php @@ -2,45 +2,31 @@ namespace PHPStan\Analyser; +use PhpParser\Node\Expr; +use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; class SpecifiedTypes { - /** @var mixed[] */ - private array $sureTypes; - - /** @var mixed[] */ - private array $sureNotTypes; - - private bool $overwrite; - - /** @var array */ - private array $newConditionalExpressionHolders; - /** * @api - * @param mixed[] $sureTypes - * @param mixed[] $sureNotTypes - * @param bool $overwrite + * @param array $sureTypes + * @param array $sureNotTypes * @param array $newConditionalExpressionHolders */ public function __construct( - array $sureTypes = [], - array $sureNotTypes = [], - bool $overwrite = false, - array $newConditionalExpressionHolders = [] + private array $sureTypes = [], + private array $sureNotTypes = [], + private bool $overwrite = false, + private array $newConditionalExpressionHolders = [], ) { - $this->sureTypes = $sureTypes; - $this->sureNotTypes = $sureNotTypes; - $this->overwrite = $overwrite; - $this->newConditionalExpressionHolders = $newConditionalExpressionHolders; } /** * @api - * @return mixed[] + * @return array */ public function getSureTypes(): array { @@ -49,7 +35,7 @@ public function getSureTypes(): array /** * @api - * @return mixed[] + * @return array */ public function getSureNotTypes(): array { @@ -131,4 +117,20 @@ public function unionWith(SpecifiedTypes $other): self return new self($sureTypeUnion, $sureNotTypeUnion); } + public function normalize(Scope $scope): self + { + $sureTypes = $this->sureTypes; + + foreach ($this->sureNotTypes as $exprString => [$exprNode, $sureNotType]) { + if (!isset($sureTypes[$exprString])) { + $sureTypes[$exprString] = [$exprNode, TypeCombinator::remove($scope->getType($exprNode), $sureNotType)]; + continue; + } + + $sureTypes[$exprString][1] = TypeCombinator::remove($sureTypes[$exprString][1], $sureNotType); + } + + return new self($sureTypes, [], $this->overwrite, $this->newConditionalExpressionHolders); + } + } diff --git a/src/Analyser/StatementExitPoint.php b/src/Analyser/StatementExitPoint.php index f5f6874438..bb372c1786 100644 --- a/src/Analyser/StatementExitPoint.php +++ b/src/Analyser/StatementExitPoint.php @@ -7,14 +7,8 @@ class StatementExitPoint { - private Stmt $statement; - - private MutatingScope $scope; - - public function __construct(Stmt $statement, MutatingScope $scope) + public function __construct(private Stmt $statement, private MutatingScope $scope) { - $this->statement = $statement; - $this->scope = $scope; } public function getStatement(): Stmt diff --git a/src/Analyser/StatementResult.php b/src/Analyser/StatementResult.php index ed4a85465f..729e7a5ec0 100644 --- a/src/Analyser/StatementResult.php +++ b/src/Analyser/StatementResult.php @@ -8,38 +8,18 @@ class StatementResult { - private MutatingScope $scope; - - private bool $hasYield; - - private bool $isAlwaysTerminating; - - /** @var StatementExitPoint[] */ - private array $exitPoints; - - /** @var ThrowPoint[] */ - private array $throwPoints; - /** - * @param MutatingScope $scope - * @param bool $hasYield - * @param bool $isAlwaysTerminating * @param StatementExitPoint[] $exitPoints * @param ThrowPoint[] $throwPoints */ public function __construct( - MutatingScope $scope, - bool $hasYield, - bool $isAlwaysTerminating, - array $exitPoints, - array $throwPoints + private MutatingScope $scope, + private bool $hasYield, + private bool $isAlwaysTerminating, + private array $exitPoints, + private array $throwPoints, ) { - $this->scope = $scope; - $this->hasYield = $hasYield; - $this->isAlwaysTerminating = $isAlwaysTerminating; - $this->exitPoints = $exitPoints; - $this->throwPoints = $throwPoints; } public function getScope(): MutatingScope @@ -136,6 +116,7 @@ public function getExitPointsForOuterLoop(): array foreach ($this->exitPoints as $exitPoint) { $statement = $exitPoint->getStatement(); if (!$statement instanceof Stmt\Continue_ && !$statement instanceof Stmt\Break_) { + $exitPoints[] = $exitPoint; continue; } if ($statement->num === null) { diff --git a/src/Analyser/ThrowPoint.php b/src/Analyser/ThrowPoint.php index 4a63f3dcce..c25c5dc05d 100644 --- a/src/Analyser/ThrowPoint.php +++ b/src/Analyser/ThrowPoint.php @@ -6,49 +6,26 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use Throwable; class ThrowPoint { - private MutatingScope $scope; - - private Type $type; - - /** @var Node\Expr|Node\Stmt */ - private Node $node; - - private bool $explicit; - - private bool $canContainAnyThrowable; - /** - * @param MutatingScope $scope - * @param Type $type * @param Node\Expr|Node\Stmt $node - * @param bool $explicit - * @param bool $canContainAnyThrowable */ private function __construct( - MutatingScope $scope, - Type $type, - Node $node, - bool $explicit, - bool $canContainAnyThrowable + private MutatingScope $scope, + private Type $type, + private Node $node, + private bool $explicit, + private bool $canContainAnyThrowable, ) { - $this->scope = $scope; - $this->type = $type; - $this->node = $node; - $this->explicit = $explicit; - $this->canContainAnyThrowable = $canContainAnyThrowable; } /** - * @param MutatingScope $scope - * @param Type $type * @param Node\Expr|Node\Stmt $node - * @param bool $canContainAnyThrowable - * @return self */ public static function createExplicit(MutatingScope $scope, Type $type, Node $node, bool $canContainAnyThrowable): self { @@ -56,13 +33,11 @@ public static function createExplicit(MutatingScope $scope, Type $type, Node $no } /** - * @param MutatingScope $scope * @param Node\Expr|Node\Stmt $node - * @return self */ public static function createImplicit(MutatingScope $scope, Node $node): self { - return new self($scope, new ObjectType(\Throwable::class), $node, false, true); + return new self($scope, new ObjectType(Throwable::class), $node, false, true); } public function getScope(): MutatingScope diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 547b4f27ef..7b95f49614 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -9,90 +9,84 @@ use PhpParser\Node\Expr\BinaryOp\BooleanOr; use PhpParser\Node\Expr\BinaryOp\LogicalAnd; use PhpParser\Node\Expr\BinaryOp\LogicalOr; +use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\Instanceof_; use PhpParser\Node\Expr\MethodCall; -use PhpParser\Node\Expr\New_; use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Expr\StaticPropertyFetch; use PhpParser\Node\Name; +use PhpParser\PrettyPrinter\Standard; +use PHPStan\Node\VirtualNode; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\HasOffsetType; use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Accessory\NonEmptyArrayType; -use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; +use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\ConstantScalarType; use PHPStan\Type\ConstantType; +use PHPStan\Type\Enum\EnumCaseObjectType; +use PHPStan\Type\FloatType; +use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\MethodTypeSpecifyingExtension; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\NonexistentParentClassType; use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\StaticMethodTypeSpecifyingExtension; use PHPStan\Type\StaticType; use PHPStan\Type\StaticTypeFactory; +use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; +use function array_merge; use function array_reverse; +use function count; +use function in_array; +use function is_string; +use function strtolower; class TypeSpecifier { - private \PhpParser\PrettyPrinter\Standard $printer; - - private ReflectionProvider $reflectionProvider; - - private bool $rememberFunctionValues; - - /** @var \PHPStan\Type\FunctionTypeSpecifyingExtension[] */ - private array $functionTypeSpecifyingExtensions; - - /** @var \PHPStan\Type\MethodTypeSpecifyingExtension[] */ - private array $methodTypeSpecifyingExtensions; - - /** @var \PHPStan\Type\StaticMethodTypeSpecifyingExtension[] */ - private array $staticMethodTypeSpecifyingExtensions; - - /** @var \PHPStan\Type\MethodTypeSpecifyingExtension[][]|null */ + /** @var MethodTypeSpecifyingExtension[][]|null */ private ?array $methodTypeSpecifyingExtensionsByClass = null; - /** @var \PHPStan\Type\StaticMethodTypeSpecifyingExtension[][]|null */ + /** @var StaticMethodTypeSpecifyingExtension[][]|null */ private ?array $staticMethodTypeSpecifyingExtensionsByClass = null; /** - * @param \PhpParser\PrettyPrinter\Standard $printer - * @param ReflectionProvider $reflectionProvider - * @param \PHPStan\Type\FunctionTypeSpecifyingExtension[] $functionTypeSpecifyingExtensions - * @param \PHPStan\Type\MethodTypeSpecifyingExtension[] $methodTypeSpecifyingExtensions - * @param \PHPStan\Type\StaticMethodTypeSpecifyingExtension[] $staticMethodTypeSpecifyingExtensions + * @param FunctionTypeSpecifyingExtension[] $functionTypeSpecifyingExtensions + * @param MethodTypeSpecifyingExtension[] $methodTypeSpecifyingExtensions + * @param StaticMethodTypeSpecifyingExtension[] $staticMethodTypeSpecifyingExtensions */ public function __construct( - \PhpParser\PrettyPrinter\Standard $printer, - ReflectionProvider $reflectionProvider, - bool $rememberFunctionValues, - array $functionTypeSpecifyingExtensions, - array $methodTypeSpecifyingExtensions, - array $staticMethodTypeSpecifyingExtensions + private Standard $printer, + private ReflectionProvider $reflectionProvider, + private array $functionTypeSpecifyingExtensions, + private array $methodTypeSpecifyingExtensions, + private array $staticMethodTypeSpecifyingExtensions, ) { - $this->printer = $printer; - $this->reflectionProvider = $reflectionProvider; - $this->rememberFunctionValues = $rememberFunctionValues; - foreach (array_merge($functionTypeSpecifyingExtensions, $methodTypeSpecifyingExtensions, $staticMethodTypeSpecifyingExtensions) as $extension) { if (!($extension instanceof TypeSpecifierAwareExtension)) { continue; @@ -100,19 +94,19 @@ public function __construct( $extension->setTypeSpecifier($this); } - - $this->functionTypeSpecifyingExtensions = $functionTypeSpecifyingExtensions; - $this->methodTypeSpecifyingExtensions = $methodTypeSpecifyingExtensions; - $this->staticMethodTypeSpecifyingExtensions = $staticMethodTypeSpecifyingExtensions; } /** @api */ public function specifyTypesInCondition( Scope $scope, Expr $expr, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): SpecifiedTypes { + if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) { + return new SpecifiedTypes(); + } + if ($expr instanceof Instanceof_) { $exprNode = $expr->expr; if ($expr->class instanceof Name) { @@ -125,7 +119,7 @@ public function specifyTypesInCondition( } elseif ($lowercasedClassName === 'parent') { if ( $scope->isInClass() - && $scope->getClassReflection()->getParentClass() !== false + && $scope->getClassReflection()->getParentClass() !== null ) { $type = new ObjectType($scope->getClassReflection()->getParentClass()->getName()); } else { @@ -158,7 +152,7 @@ public function specifyTypesInCondition( if ($context->true()) { $type = TypeCombinator::intersect( $type, - new ObjectWithoutClassType() + new ObjectWithoutClassType(), ); return $this->create($exprNode, $type, $context, false, $scope); } elseif ($context->false()) { @@ -176,14 +170,14 @@ public function specifyTypesInCondition( if ($expressions !== null) { /** @var Expr $exprNode */ $exprNode = $expressions[0]; - /** @var \PHPStan\Type\ConstantScalarType $constantType */ + /** @var ConstantScalarType $constantType */ $constantType = $expressions[1]; if ($constantType->getValue() === false) { $types = $this->create($exprNode, $constantType, $context, false, $scope); return $types->unionWith($this->specifyTypesInCondition( $scope, $exprNode, - $context->true() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createFalse()->negate() + $context->true() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createFalse()->negate(), )); } @@ -192,7 +186,7 @@ public function specifyTypesInCondition( return $types->unionWith($this->specifyTypesInCondition( $scope, $exprNode, - $context->true() ? TypeSpecifierContext::createTrue() : TypeSpecifierContext::createTrue()->negate() + $context->true() ? TypeSpecifierContext::createTrue() : TypeSpecifierContext::createTrue()->negate(), )); } @@ -203,9 +197,9 @@ public function specifyTypesInCondition( if ( !$context->null() && $exprNode instanceof FuncCall - && count($exprNode->args) === 1 + && count($exprNode->getArgs()) === 1 && $exprNode->name instanceof Name - && strtolower((string) $exprNode->name) === 'count' + && in_array(strtolower((string) $exprNode->name), ['count', 'sizeof'], true) && $constantType instanceof ConstantIntegerType ) { if ($context->truthy() || $constantType->getValue() === 0) { @@ -213,21 +207,57 @@ public function specifyTypesInCondition( if ($constantType->getValue() === 0) { $newContext = $newContext->negate(); } - $argType = $scope->getType($exprNode->args[0]->value); + $argType = $scope->getType($exprNode->getArgs()[0]->value); if ($argType->isArray()->yes()) { - return $this->create($exprNode->args[0]->value, new NonEmptyArrayType(), $newContext, false, $scope); + $funcTypes = $this->create($exprNode, $constantType, $context, false, $scope); + $valueTypes = $this->create($exprNode->getArgs()[0]->value, new NonEmptyArrayType(), $newContext, false, $scope); + return $funcTypes->unionWith($valueTypes); } } } - } - if ($context->true()) { - $type = TypeCombinator::intersect($scope->getType($expr->right), $scope->getType($expr->left)); - $leftTypes = $this->create($expr->left, $type, $context, false, $scope); - $rightTypes = $this->create($expr->right, $type, $context, false, $scope); - return $leftTypes->unionWith($rightTypes); + if ( + !$context->null() + && $exprNode instanceof FuncCall + && count($exprNode->getArgs()) === 1 + && $exprNode->name instanceof Name + && strtolower((string) $exprNode->name) === 'strlen' + && $constantType instanceof ConstantIntegerType + ) { + if ($context->truthy() || $constantType->getValue() === 0) { + $newContext = $context; + if ($constantType->getValue() === 0) { + $newContext = $newContext->negate(); + } + $argType = $scope->getType($exprNode->getArgs()[0]->value); + if ($argType instanceof StringType) { + $funcTypes = $this->create($exprNode, $constantType, $context, false, $scope); + $valueTypes = $this->create($exprNode->getArgs()[0]->value, new AccessoryNonEmptyStringType(), $newContext, false, $scope); + return $funcTypes->unionWith($valueTypes); + } + } + } + } - } elseif ($context->false()) { + $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, + ); + } + if ($context->false()) { $identicalType = $scope->getType($expr); if ($identicalType instanceof ConstantBooleanType) { $never = new NeverType(); @@ -236,34 +266,30 @@ public function specifyTypesInCondition( $rightTypes = $this->create($expr->right, $never, $contextForTypes, false, $scope); return $leftTypes->unionWith($rightTypes); } + } - $exprLeftType = $scope->getType($expr->left); - $exprRightType = $scope->getType($expr->right); - - $types = null; - - if ( - $exprLeftType instanceof ConstantType - && !$expr->right instanceof Node\Scalar - ) { + $types = null; + $exprLeftType = $scope->getType($expr->left); + $exprRightType = $scope->getType($expr->right); + if ($exprLeftType instanceof ConstantType || $exprLeftType instanceof EnumCaseObjectType) { + if (!$expr->right instanceof Node\Scalar && !$expr->right instanceof Expr\Array_) { $types = $this->create( $expr->right, $exprLeftType, $context, false, - $scope + $scope, ); } - if ( - $exprRightType instanceof ConstantType - && !$expr->left instanceof Node\Scalar - ) { + } + if ($exprRightType instanceof ConstantType || $exprRightType instanceof EnumCaseObjectType) { + if ($types === null || (!$expr->left instanceof Node\Scalar && !$expr->left instanceof Expr\Array_)) { $leftType = $this->create( $expr->left, $exprRightType, $context, false, - $scope + $scope, ); if ($types !== null) { $types = $types->unionWith($leftType); @@ -271,30 +297,48 @@ public function specifyTypesInCondition( $types = $leftType; } } + } + + if ($types !== null) { + return $types; + } - if ($types !== null) { - return $types; + $leftExprString = $this->printer->prettyPrintExpr($expr->left); + $rightExprString = $this->printer->prettyPrintExpr($expr->right); + if ($leftExprString === $rightExprString) { + if (!$expr->left instanceof Expr\Variable || !$expr->right instanceof Expr\Variable) { + return new SpecifiedTypes(); } } + if ($context->true()) { + $type = TypeCombinator::intersect($scope->getType($expr->right), $scope->getType($expr->left)); + $leftTypes = $this->create($expr->left, $type, $context, false, $scope); + $rightTypes = $this->create($expr->right, $type, $context, false, $scope); + return $leftTypes->unionWith($rightTypes); + } elseif ($context->false()) { + return $this->create($expr->left, $exprLeftType, $context, false, $scope)->normalize($scope) + ->intersectWith($this->create($expr->right, $exprRightType, $context, false, $scope)->normalize($scope)); + } + } elseif ($expr instanceof Node\Expr\BinaryOp\NotIdentical) { return $this->specifyTypesInCondition( $scope, new Node\Expr\BooleanNot(new Node\Expr\BinaryOp\Identical($expr->left, $expr->right)), - $context + $context, ); } elseif ($expr instanceof Node\Expr\BinaryOp\Equal) { $expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr); if ($expressions !== null) { /** @var Expr $exprNode */ $exprNode = $expressions[0]; - /** @var \PHPStan\Type\ConstantScalarType $constantType */ + /** @var ConstantScalarType $constantType */ $constantType = $expressions[1]; if ($constantType->getValue() === false || $constantType->getValue() === null) { return $this->specifyTypesInCondition( $scope, $exprNode, - $context->true() ? TypeSpecifierContext::createFalsey() : TypeSpecifierContext::createFalsey()->negate() + $context->true() ? TypeSpecifierContext::createFalsey() : TypeSpecifierContext::createFalsey()->negate(), ); } @@ -302,22 +346,23 @@ public function specifyTypesInCondition( return $this->specifyTypesInCondition( $scope, $exprNode, - $context->true() ? TypeSpecifierContext::createTruthy() : TypeSpecifierContext::createTruthy()->negate() + $context->true() ? TypeSpecifierContext::createTruthy() : TypeSpecifierContext::createTruthy()->negate(), ); } } $leftType = $scope->getType($expr->left); - $leftBooleanType = $leftType->toBoolean(); $rightType = $scope->getType($expr->right); + + $leftBooleanType = $leftType->toBoolean(); if ($leftBooleanType instanceof ConstantBooleanType && $rightType instanceof BooleanType) { return $this->specifyTypesInCondition( $scope, new Expr\BinaryOp\Identical( new ConstFetch(new Name($leftBooleanType->getValue() ? 'true' : 'false')), - $expr->right + $expr->right, ), - $context + $context, ); } @@ -327,26 +372,40 @@ public function specifyTypesInCondition( $scope, new Expr\BinaryOp\Identical( $expr->left, - new ConstFetch(new Name($rightBooleanType->getValue() ? 'true' : 'false')) + new ConstFetch(new Name($rightBooleanType->getValue() ? 'true' : 'false')), ), - $context + $context, ); } + if ( + $rightType->isArray()->yes() + && $leftType instanceof ConstantArrayType && $leftType->isEmpty() + ) { + return $this->create($expr->right, new NonEmptyArrayType(), $context->negate(), false, $scope); + } + + if ( + $leftType->isArray()->yes() + && $rightType instanceof ConstantArrayType && $rightType->isEmpty() + ) { + return $this->create($expr->left, new NonEmptyArrayType(), $context->negate(), false, $scope); + } + if ( $expr->left instanceof FuncCall && $expr->left->name instanceof Name && strtolower($expr->left->name->toString()) === 'get_class' - && isset($expr->left->args[0]) + && isset($expr->left->getArgs()[0]) && $rightType instanceof ConstantStringType ) { return $this->specifyTypesInCondition( $scope, new Instanceof_( - $expr->left->args[0]->value, - new Name($rightType->getValue()) + $expr->left->getArgs()[0]->value, + new Name($rightType->getValue()), ), - $context + $context, ); } @@ -354,23 +413,49 @@ public function specifyTypesInCondition( $expr->right instanceof FuncCall && $expr->right->name instanceof Name && strtolower($expr->right->name->toString()) === 'get_class' - && isset($expr->right->args[0]) + && isset($expr->right->getArgs()[0]) && $leftType instanceof ConstantStringType ) { return $this->specifyTypesInCondition( $scope, new Instanceof_( - $expr->right->args[0]->value, - new Name($leftType->getValue()) + $expr->right->getArgs()[0]->value, + new Name($leftType->getValue()), ), - $context + $context, ); } + + $stringType = new StringType(); + $integerType = new IntegerType(); + $floatType = new FloatType(); + if ( + ($stringType->isSuperTypeOf($leftType)->yes() && $stringType->isSuperTypeOf($rightType)->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); + } + + $leftExprString = $this->printer->prettyPrintExpr($expr->left); + $rightExprString = $this->printer->prettyPrintExpr($expr->right); + if ($leftExprString === $rightExprString) { + if (!$expr->left instanceof Expr\Variable || !$expr->right instanceof Expr\Variable) { + return new SpecifiedTypes(); + } + } + + $leftTypes = $this->create($expr->left, $leftType, $context, false, $scope); + $rightTypes = $this->create($expr->right, $rightType, $context, false, $scope); + + return $context->true() + ? $leftTypes->unionWith($rightTypes) + : $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($scope)); } elseif ($expr instanceof Node\Expr\BinaryOp\NotEqual) { return $this->specifyTypesInCondition( $scope, new Node\Expr\BooleanNot(new Node\Expr\BinaryOp\Equal($expr->left, $expr->right)), - $context + $context, ); } elseif ($expr instanceof Node\Expr\BinaryOp\Smaller || $expr instanceof Node\Expr\BinaryOp\SmallerOrEqual) { @@ -381,13 +466,13 @@ public function specifyTypesInCondition( if ( $expr->left instanceof FuncCall - && count($expr->left->args) === 1 + && count($expr->left->getArgs()) === 1 && $expr->left->name instanceof Name - && strtolower((string) $expr->left->name) === 'count' + && in_array(strtolower((string) $expr->left->name), ['count', 'sizeof', 'strlen'], true) && ( !$expr->right instanceof FuncCall || !$expr->right->name instanceof Name - || strtolower((string) $expr->right->name) !== 'count' + || !in_array(strtolower((string) $expr->right->name), ['count', 'sizeof', 'strlen'], true) ) ) { $inverseOperator = $expr instanceof Node\Expr\BinaryOp\Smaller @@ -397,7 +482,7 @@ public function specifyTypesInCondition( return $this->specifyTypesInCondition( $scope, new Node\Expr\BooleanNot($inverseOperator), - $context + $context, ); } @@ -406,18 +491,37 @@ public function specifyTypesInCondition( if ( !$context->null() && $expr->right instanceof FuncCall - && count($expr->right->args) === 1 + && count($expr->right->getArgs()) === 1 && $expr->right->name instanceof Name - && strtolower((string) $expr->right->name) === 'count' + && in_array(strtolower((string) $expr->right->name), ['count', 'sizeof'], true) && (new IntegerType())->isSuperTypeOf($leftType)->yes() ) { if ( $context->truthy() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) || ($context->falsey() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) ) { - $argType = $scope->getType($expr->right->args[0]->value); + $argType = $scope->getType($expr->right->getArgs()[0]->value); if ($argType->isArray()->yes()) { - $result = $result->unionWith($this->create($expr->right->args[0]->value, new NonEmptyArrayType(), $context, false, $scope)); + $result = $result->unionWith($this->create($expr->right->getArgs()[0]->value, new NonEmptyArrayType(), $context, false, $scope)); + } + } + } + + if ( + !$context->null() + && $expr->right instanceof FuncCall + && count($expr->right->getArgs()) === 1 + && $expr->right->name instanceof Name + && strtolower((string) $expr->right->name) === 'strlen' + && (new IntegerType())->isSuperTypeOf($leftType)->yes() + ) { + if ( + $context->truthy() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) + || ($context->falsey() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) + ) { + $argType = $scope->getType($expr->right->getArgs()[0]->value); + if ($argType instanceof StringType) { + $result = $result->unionWith($this->create($expr->right->getArgs()[0]->value, new AccessoryNonEmptyStringType(), $context, false, $scope)); } } } @@ -427,19 +531,19 @@ public function specifyTypesInCondition( $result = $result->unionWith($this->createRangeTypes( $expr->right->var, IntegerRangeType::fromInterval($leftType->getValue(), null, $offset + 1), - $context + $context, )); } elseif ($expr->right instanceof Expr\PostDec) { $result = $result->unionWith($this->createRangeTypes( $expr->right->var, IntegerRangeType::fromInterval($leftType->getValue(), null, $offset - 1), - $context + $context, )); } elseif ($expr->right instanceof Expr\PreInc || $expr->right instanceof Expr\PreDec) { $result = $result->unionWith($this->createRangeTypes( $expr->right->var, IntegerRangeType::fromInterval($leftType->getValue(), null, $offset), - $context + $context, )); } } @@ -449,19 +553,19 @@ public function specifyTypesInCondition( $result = $result->unionWith($this->createRangeTypes( $expr->left->var, IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset + 1), - $context + $context, )); } elseif ($expr->left instanceof Expr\PostDec) { $result = $result->unionWith($this->createRangeTypes( $expr->left->var, IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset - 1), - $context + $context, )); } elseif ($expr->left instanceof Expr\PreInc || $expr->left instanceof Expr\PreDec) { $result = $result->unionWith($this->createRangeTypes( $expr->left->var, IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset), - $context + $context, )); } } @@ -474,8 +578,8 @@ public function specifyTypesInCondition( $orEqual ? $rightType->getSmallerOrEqualType() : $rightType->getSmallerType(), TypeSpecifierContext::createTruthy(), false, - $scope - ) + $scope, + ), ); } if (!$expr->right instanceof Node\Scalar) { @@ -485,8 +589,8 @@ public function specifyTypesInCondition( $orEqual ? $leftType->getGreaterOrEqualType() : $leftType->getGreaterType(), TypeSpecifierContext::createTruthy(), false, - $scope - ) + $scope, + ), ); } } elseif ($context->false()) { @@ -497,8 +601,8 @@ public function specifyTypesInCondition( $orEqual ? $rightType->getGreaterType() : $rightType->getGreaterOrEqualType(), TypeSpecifierContext::createTruthy(), false, - $scope - ) + $scope, + ), ); } if (!$expr->right instanceof Node\Scalar) { @@ -508,8 +612,8 @@ public function specifyTypesInCondition( $orEqual ? $leftType->getSmallerType() : $leftType->getSmallerOrEqualType(), TypeSpecifierContext::createTruthy(), false, - $scope - ) + $scope, + ), ); } } @@ -534,9 +638,7 @@ public function specifyTypesInCondition( } } - if ($this->rememberFunctionValues) { - return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); - } + return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); } elseif ($expr instanceof MethodCall && $expr->name instanceof Node\Identifier) { $methodCalledOnType = $scope->getType($expr->var); $referencedClasses = TypeUtils::getDirectClassNames($methodCalledOnType); @@ -557,9 +659,7 @@ public function specifyTypesInCondition( } } - if ($this->rememberFunctionValues) { - return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); - } + return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); } elseif ($expr instanceof StaticCall && $expr->name instanceof Node\Identifier) { if ($expr->class instanceof Name) { $calleeType = $scope->resolveTypeByName($expr->class); @@ -585,13 +685,15 @@ public function specifyTypesInCondition( } } - if ($this->rememberFunctionValues) { - return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); - } + return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); } elseif ($expr instanceof BooleanAnd || $expr instanceof LogicalAnd) { + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); + } $leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context); - $rightTypes = $this->specifyTypesInCondition($scope, $expr->right, $context); - $types = $context->true() ? $leftTypes->unionWith($rightTypes) : $leftTypes->intersectWith($rightTypes); + $rightScope = $scope->filterByTruthyValue($expr->left); + $rightTypes = $this->specifyTypesInCondition($rightScope, $expr->right, $context); + $types = $context->true() ? $leftTypes->unionWith($rightTypes) : $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($rightScope)); if ($context->false()) { return new SpecifiedTypes( $types->getSureTypes(), @@ -599,16 +701,20 @@ public function specifyTypesInCondition( false, array_merge( $this->processBooleanConditionalTypes($scope, $leftTypes, $rightTypes), - $this->processBooleanConditionalTypes($scope, $rightTypes, $leftTypes) - ) + $this->processBooleanConditionalTypes($scope, $rightTypes, $leftTypes), + ), ); } return $types; } elseif ($expr instanceof BooleanOr || $expr instanceof LogicalOr) { + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); + } $leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context); - $rightTypes = $this->specifyTypesInCondition($scope, $expr->right, $context); - $types = $context->true() ? $leftTypes->intersectWith($rightTypes) : $leftTypes->unionWith($rightTypes); + $rightScope = $scope->filterByFalseyValue($expr->left); + $rightTypes = $this->specifyTypesInCondition($rightScope, $expr->right, $context); + $types = $context->true() ? $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($rightScope)) : $leftTypes->unionWith($rightTypes); if ($context->true()) { return new SpecifiedTypes( $types->getSureTypes(), @@ -616,8 +722,8 @@ public function specifyTypesInCondition( false, array_merge( $this->processBooleanConditionalTypes($scope, $leftTypes, $rightTypes), - $this->processBooleanConditionalTypes($scope, $rightTypes, $leftTypes) - ) + $this->processBooleanConditionalTypes($scope, $rightTypes, $leftTypes), + ), ); } @@ -626,7 +732,7 @@ public function specifyTypesInCondition( return $this->specifyTypesInCondition($scope, $expr->expr, $context->negate()); } elseif ($expr instanceof Node\Expr\Assign) { if (!$scope instanceof MutatingScope) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ($context->null()) { return $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->expr, $context); @@ -634,20 +740,12 @@ public function specifyTypesInCondition( return $this->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->var, $context); } elseif ( - ( - $expr instanceof Expr\Isset_ - && count($expr->vars) > 0 - && $context->true() - ) - || ($expr instanceof Expr\Empty_ && $context->false()) + $expr instanceof Expr\Isset_ + && count($expr->vars) > 0 + && $context->true() ) { $vars = []; - if ($expr instanceof Expr\Isset_) { - $varsToIterate = $expr->vars; - } else { - $varsToIterate = [$expr->expr]; - } - foreach ($varsToIterate as $var) { + foreach ($expr->vars as $var) { $tmpVars = [$var]; while ( @@ -671,7 +769,7 @@ public function specifyTypesInCondition( } if (count($vars) === 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $types = null; @@ -681,35 +779,22 @@ public function specifyTypesInCondition( return new SpecifiedTypes([], []); } } - if ($expr instanceof Expr\Isset_) { - if ( - $var instanceof ArrayDimFetch - && $var->dim !== null - && !$scope->getType($var->var) instanceof MixedType - ) { - $type = $this->create( - $var->var, - new HasOffsetType($scope->getType($var->dim)), - $context, - false, - $scope - )->unionWith( - $this->create($var, new NullType(), TypeSpecifierContext::createFalse(), false, $scope) - ); - } else { - $type = $this->create($var, new NullType(), TypeSpecifierContext::createFalse(), false, $scope); - } - } else { + if ( + $var instanceof ArrayDimFetch + && $var->dim !== null + && !$scope->getType($var->var) instanceof MixedType + ) { $type = $this->create( - $var, - new UnionType([ - new NullType(), - new ConstantBooleanType(false), - ]), - TypeSpecifierContext::createFalse(), + $var->var, + new HasOffsetType($scope->getType($var->dim)), + $context, false, - $scope + $scope, + )->unionWith( + $this->create($var, new NullType(), TypeSpecifierContext::createFalse(), false, $scope), ); + } else { + $type = $this->create($var, new NullType(), TypeSpecifierContext::createFalse(), false, $scope); } if ( @@ -738,14 +823,6 @@ public function specifyTypesInCondition( } } - if ( - $expr instanceof Expr\Empty_ - && (new ArrayType(new MixedType(), new MixedType()))->isSuperTypeOf($scope->getType($expr->expr))->yes()) { - $types = $types->unionWith( - $this->create($expr->expr, new NonEmptyArrayType(), $context->negate(), false, $scope) - ); - } - return $types; } elseif ( $expr instanceof Expr\BinaryOp\Coalesce @@ -757,13 +834,15 @@ public function specifyTypesInCondition( new NullType(), TypeSpecifierContext::createFalse(), false, - $scope + $scope, ); } elseif ( - $expr instanceof Expr\Empty_ && $context->truthy() - && (new ArrayType(new MixedType(), new MixedType()))->isSuperTypeOf($scope->getType($expr->expr))->yes() + $expr instanceof Expr\Empty_ ) { - return $this->create($expr->expr, new NonEmptyArrayType(), $context->negate(), false, $scope); + return $this->specifyTypesInCondition($scope, new BooleanOr( + new Expr\BooleanNot(new Expr\Isset_([$expr->expr])), + new Expr\BooleanNot($expr->expr), + ), $context); } elseif ($expr instanceof Expr\ErrorSuppress) { return $this->specifyTypesInCondition($scope, $expr->expr, $context); } elseif ( @@ -783,25 +862,25 @@ public function specifyTypesInCondition( $scope, new BooleanAnd( new Expr\BinaryOp\NotIdentical($expr->var, new ConstFetch(new Name('null'))), - new PropertyFetch($expr->var, $expr->name) + new PropertyFetch($expr->var, $expr->name), ), - $context + $context, ); $nullSafeTypes = $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); - return $context->true() ? $types->unionWith($nullSafeTypes) : $types->intersectWith($nullSafeTypes); + return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($scope)->intersectWith($nullSafeTypes->normalize($scope)); } elseif ($expr instanceof Expr\NullsafeMethodCall && !$context->null()) { $types = $this->specifyTypesInCondition( $scope, new BooleanAnd( new Expr\BinaryOp\NotIdentical($expr->var, new ConstFetch(new Name('null'))), - new MethodCall($expr->var, $expr->name, $expr->args) + new MethodCall($expr->var, $expr->name, $expr->args), ), - $context + $context, ); $nullSafeTypes = $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); - return $context->true() ? $types->unionWith($nullSafeTypes) : $types->intersectWith($nullSafeTypes); + return $context->true() ? $types->unionWith($nullSafeTypes) : $types->normalize($scope)->intersectWith($nullSafeTypes->normalize($scope)); } elseif (!$context->null()) { return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); } @@ -826,9 +905,6 @@ private function handleDefaultTruthyOrFalseyContext(TypeSpecifierContext $contex } /** - * @param Scope $scope - * @param SpecifiedTypes $leftTypes - * @param SpecifiedTypes $rightTypes * @return array */ private function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes): array @@ -861,7 +937,7 @@ private function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $le $holders[$exprString][] = new ConditionalExpressionHolder( $conditionExpressionTypes, - new VariableTypeHolder(TypeCombinator::remove($scope->getType($expr), $type), TrinaryLogic::createYes()) + new VariableTypeHolder(TypeCombinator::remove($scope->getType($expr), $type), TrinaryLogic::createYes()), ); } @@ -872,24 +948,22 @@ private function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $le } /** - * @param \PHPStan\Analyser\Scope $scope - * @param \PhpParser\Node\Expr\BinaryOp $binaryOperation - * @return (Expr|\PHPStan\Type\ConstantScalarType)[]|null + * @return (Expr|ConstantScalarType)[]|null */ private function findTypeExpressionsFromBinaryOperation(Scope $scope, Node\Expr\BinaryOp $binaryOperation): ?array { $leftType = $scope->getType($binaryOperation->left); $rightType = $scope->getType($binaryOperation->right); if ( - $leftType instanceof \PHPStan\Type\ConstantScalarType + $leftType instanceof ConstantScalarType && !$binaryOperation->right instanceof ConstFetch - && !$binaryOperation->right instanceof Expr\ClassConstFetch + && !$binaryOperation->right instanceof ClassConstFetch ) { return [$binaryOperation->right, $leftType]; } elseif ( - $rightType instanceof \PHPStan\Type\ConstantScalarType + $rightType instanceof ConstantScalarType && !$binaryOperation->left instanceof ConstFetch - && !$binaryOperation->left instanceof Expr\ClassConstFetch + && !$binaryOperation->left instanceof ClassConstFetch ) { return [$binaryOperation->left, $rightType]; } @@ -903,10 +977,10 @@ public function create( Type $type, TypeSpecifierContext $context, bool $overwrite = false, - ?Scope $scope = null + ?Scope $scope = null, ): SpecifiedTypes { - if ($expr instanceof New_ || $expr instanceof Instanceof_) { + if ($expr instanceof Instanceof_ || $expr instanceof Expr\List_ || $expr instanceof VirtualNode) { return new SpecifiedTypes(); } @@ -987,7 +1061,7 @@ private function createNullsafeTypes(Expr $expr, Scope $scope, TypeSpecifierCont } return $propertyFetchTypes->unionWith( - $this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), false, $scope) + $this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), false, $scope), ); } @@ -999,7 +1073,7 @@ private function createNullsafeTypes(Expr $expr, Scope $scope, TypeSpecifierCont } return $methodCallTypes->unionWith( - $this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), false, $scope) + $this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), false, $scope), ); } @@ -1044,7 +1118,7 @@ private function createRangeTypes(Expr $expr, Type $type, TypeSpecifierContext $ } /** - * @return \PHPStan\Type\FunctionTypeSpecifyingExtension[] + * @return FunctionTypeSpecifyingExtension[] */ private function getFunctionTypeSpecifyingExtensions(): array { @@ -1052,8 +1126,7 @@ private function getFunctionTypeSpecifyingExtensions(): array } /** - * @param string $className - * @return \PHPStan\Type\MethodTypeSpecifyingExtension[] + * @return MethodTypeSpecifyingExtension[] */ private function getMethodTypeSpecifyingExtensionsForClass(string $className): array { @@ -1069,8 +1142,7 @@ private function getMethodTypeSpecifyingExtensionsForClass(string $className): a } /** - * @param string $className - * @return \PHPStan\Type\StaticMethodTypeSpecifyingExtension[] + * @return StaticMethodTypeSpecifyingExtension[] */ private function getStaticMethodTypeSpecifyingExtensionsForClass(string $className): array { @@ -1086,8 +1158,7 @@ private function getStaticMethodTypeSpecifyingExtensionsForClass(string $classNa } /** - * @param \PHPStan\Type\MethodTypeSpecifyingExtension[][]|\PHPStan\Type\StaticMethodTypeSpecifyingExtension[][] $extensions - * @param string $className + * @param MethodTypeSpecifyingExtension[][]|StaticMethodTypeSpecifyingExtension[][] $extensions * @return mixed[] */ private function getTypeSpecifyingExtensionsForType(array $extensions, string $className): array diff --git a/src/Analyser/TypeSpecifierContext.php b/src/Analyser/TypeSpecifierContext.php index 7a5feba27e..a14aba7112 100644 --- a/src/Analyser/TypeSpecifierContext.php +++ b/src/Analyser/TypeSpecifierContext.php @@ -2,6 +2,8 @@ namespace PHPStan\Analyser; +use PHPStan\ShouldNotHappenException; + /** @api */ class TypeSpecifierContext { @@ -12,20 +14,18 @@ class TypeSpecifierContext public const CONTEXT_FALSE = 0b0100; public const CONTEXT_FALSEY_BUT_NOT_FALSE = 0b1000; public const CONTEXT_FALSEY = self::CONTEXT_FALSE | self::CONTEXT_FALSEY_BUT_NOT_FALSE; - - private ?int $value; + public const CONTEXT_BITMASK = 0b1111; /** @var self[] */ private static array $registry; - private function __construct(?int $value) + private function __construct(private ?int $value) { - $this->value = $value; } private static function create(?int $value): self { - self::$registry[$value] = self::$registry[$value] ?? new self($value); + self::$registry[$value] ??= new self($value); return self::$registry[$value]; } @@ -57,9 +57,9 @@ public static function createNull(): self public function negate(): self { if ($this->value === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - return self::create(~$this->value); + return self::create(~$this->value & self::CONTEXT_BITMASK); } public function true(): bool diff --git a/src/Analyser/TypeSpecifierFactory.php b/src/Analyser/TypeSpecifierFactory.php index dc022b914d..17d638f457 100644 --- a/src/Analyser/TypeSpecifierFactory.php +++ b/src/Analyser/TypeSpecifierFactory.php @@ -6,6 +6,7 @@ use PHPStan\Broker\BrokerFactory; use PHPStan\DependencyInjection\Container; use PHPStan\Reflection\ReflectionProvider; +use function array_merge; class TypeSpecifierFactory { @@ -14,11 +15,8 @@ class TypeSpecifierFactory public const METHOD_TYPE_SPECIFYING_EXTENSION_TAG = 'phpstan.typeSpecifier.methodTypeSpecifyingExtension'; public const STATIC_METHOD_TYPE_SPECIFYING_EXTENSION_TAG = 'phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension'; - private \PHPStan\DependencyInjection\Container $container; - - public function __construct(Container $container) + public function __construct(private Container $container) { - $this->container = $container; } public function create(): TypeSpecifier @@ -26,10 +24,9 @@ public function create(): TypeSpecifier $typeSpecifier = new TypeSpecifier( $this->container->getByType(Standard::class), $this->container->getByType(ReflectionProvider::class), - $this->container->getParameter('featureToggles')['rememberFunctionValues'], $this->container->getServicesByTag(self::FUNCTION_TYPE_SPECIFYING_EXTENSION_TAG), $this->container->getServicesByTag(self::METHOD_TYPE_SPECIFYING_EXTENSION_TAG), - $this->container->getServicesByTag(self::STATIC_METHOD_TYPE_SPECIFYING_EXTENSION_TAG) + $this->container->getServicesByTag(self::STATIC_METHOD_TYPE_SPECIFYING_EXTENSION_TAG), ); foreach (array_merge( @@ -37,7 +34,7 @@ public function create(): TypeSpecifier $this->container->getServicesByTag(BrokerFactory::METHODS_CLASS_REFLECTION_EXTENSION_TAG), $this->container->getServicesByTag(BrokerFactory::DYNAMIC_METHOD_RETURN_TYPE_EXTENSION_TAG), $this->container->getServicesByTag(BrokerFactory::DYNAMIC_STATIC_METHOD_RETURN_TYPE_EXTENSION_TAG), - $this->container->getServicesByTag(BrokerFactory::DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG) + $this->container->getServicesByTag(BrokerFactory::DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG), ) as $extension) { if (!($extension instanceof TypeSpecifierAwareExtension)) { continue; diff --git a/src/Analyser/UndefinedVariableException.php b/src/Analyser/UndefinedVariableException.php index 55be8974c0..6719de349c 100644 --- a/src/Analyser/UndefinedVariableException.php +++ b/src/Analyser/UndefinedVariableException.php @@ -2,18 +2,15 @@ namespace PHPStan\Analyser; -class UndefinedVariableException extends \PHPStan\AnalysedCodeException -{ - - private \PHPStan\Analyser\Scope $scope; +use PHPStan\AnalysedCodeException; +use function sprintf; - private string $variableName; +class UndefinedVariableException extends AnalysedCodeException +{ - public function __construct(Scope $scope, string $variableName) + public function __construct(private Scope $scope, private string $variableName) { parent::__construct(sprintf('Undefined variable: $%s', $variableName)); - $this->scope = $scope; - $this->variableName = $variableName; } public function getScope(): Scope diff --git a/src/Analyser/VariableTypeHolder.php b/src/Analyser/VariableTypeHolder.php index aba3d385d7..70273de965 100644 --- a/src/Analyser/VariableTypeHolder.php +++ b/src/Analyser/VariableTypeHolder.php @@ -9,14 +9,8 @@ class VariableTypeHolder { - private \PHPStan\Type\Type $type; - - private \PHPStan\TrinaryLogic $certainty; - - public function __construct(Type $type, TrinaryLogic $certainty) + public function __construct(private Type $type, private TrinaryLogic $certainty) { - $this->type = $type; - $this->certainty = $certainty; } public static function createYes(Type $type): self @@ -47,7 +41,7 @@ public function and(self $other): self } return new self( $type, - $this->getCertainty()->and($other->getCertainty()) + $this->getCertainty()->and($other->getCertainty()), ); } diff --git a/src/Broker/AnonymousClassNameHelper.php b/src/Broker/AnonymousClassNameHelper.php index b7040689a2..b0e9bdfb40 100644 --- a/src/Broker/AnonymousClassNameHelper.php +++ b/src/Broker/AnonymousClassNameHelper.php @@ -2,41 +2,39 @@ namespace PHPStan\Broker; +use PhpParser\Node; use PHPStan\File\FileHelper; use PHPStan\File\RelativePathHelper; +use PHPStan\ShouldNotHappenException; +use function md5; +use function sprintf; class AnonymousClassNameHelper { - private FileHelper $fileHelper; - - private RelativePathHelper $relativePathHelper; - public function __construct( - FileHelper $fileHelper, - RelativePathHelper $relativePathHelper + private FileHelper $fileHelper, + private RelativePathHelper $relativePathHelper, ) { - $this->fileHelper = $fileHelper; - $this->relativePathHelper = $relativePathHelper; } public function getAnonymousClassName( - \PhpParser\Node\Stmt\Class_ $classNode, - string $filename + Node\Stmt\Class_ $classNode, + string $filename, ): string { if (isset($classNode->namespacedName)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $filename = $this->relativePathHelper->getRelativePath( - $this->fileHelper->normalizePath($filename, '/') + $this->fileHelper->normalizePath($filename, '/'), ); return sprintf( 'AnonymousClass%s', - md5(sprintf('%s:%s', $filename, $classNode->getLine())) + md5(sprintf('%s:%s', $filename, $classNode->getLine())), ); } diff --git a/src/Broker/Broker.php b/src/Broker/Broker.php index 4422e22e05..871078e13d 100644 --- a/src/Broker/Broker.php +++ b/src/Broker/Broker.php @@ -2,176 +2,142 @@ namespace PHPStan\Broker; +use PhpParser\Node; use PHPStan\Analyser\Scope; -use PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider; -use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\GlobalConstantReflection; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Type\OperatorTypeSpecifyingExtension; -use PHPStan\Type\Type; +use PHPStan\ShouldNotHappenException; /** @api */ class Broker implements ReflectionProvider { - private ReflectionProvider $reflectionProvider; - - private DynamicReturnTypeExtensionRegistryProvider $dynamicReturnTypeExtensionRegistryProvider; - - private \PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider $operatorTypeSpecifyingExtensionRegistryProvider; - - /** @var string[] */ - private array $universalObjectCratesClasses; - - private static ?\PHPStan\Broker\Broker $instance = null; + private static ?Broker $instance = null; /** - * @param \PHPStan\Reflection\ReflectionProvider $reflectionProvider - * @param \PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider $dynamicReturnTypeExtensionRegistryProvider - * @param \PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider $operatorTypeSpecifyingExtensionRegistryProvider * @param string[] $universalObjectCratesClasses */ public function __construct( - ReflectionProvider $reflectionProvider, - DynamicReturnTypeExtensionRegistryProvider $dynamicReturnTypeExtensionRegistryProvider, - OperatorTypeSpecifyingExtensionRegistryProvider $operatorTypeSpecifyingExtensionRegistryProvider, - array $universalObjectCratesClasses + private ReflectionProvider $reflectionProvider, + private array $universalObjectCratesClasses, ) { - $this->reflectionProvider = $reflectionProvider; - $this->dynamicReturnTypeExtensionRegistryProvider = $dynamicReturnTypeExtensionRegistryProvider; - $this->operatorTypeSpecifyingExtensionRegistryProvider = $operatorTypeSpecifyingExtensionRegistryProvider; - $this->universalObjectCratesClasses = $universalObjectCratesClasses; } - public static function registerInstance(Broker $reflectionProvider): void + public static function registerInstance(Broker $broker): void { - self::$instance = $reflectionProvider; + self::$instance = $broker; } + /** + * @deprecated Use PHPStan\Reflection\ReflectionProviderStaticAccessor instead + */ public static function getInstance(): Broker { if (self::$instance === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return self::$instance; } + /** + * @deprecated Use PHPStan\Reflection\ReflectionProvider instead + */ public function hasClass(string $className): bool { return $this->reflectionProvider->hasClass($className); } + /** + * @deprecated Use PHPStan\Reflection\ReflectionProvider instead + */ public function getClass(string $className): ClassReflection { return $this->reflectionProvider->getClass($className); } + /** + * @deprecated Use PHPStan\Reflection\ReflectionProvider instead + */ public function getClassName(string $className): string { return $this->reflectionProvider->getClassName($className); } + /** + * @deprecated Use PHPStan\Reflection\ReflectionProvider instead + */ public function supportsAnonymousClasses(): bool { return $this->reflectionProvider->supportsAnonymousClasses(); } - public function getAnonymousClassReflection(\PhpParser\Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection + /** + * @deprecated Use PHPStan\Reflection\ReflectionProvider instead + */ + public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection { return $this->reflectionProvider->getAnonymousClassReflection($classNode, $scope); } - public function hasFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool - { - return $this->reflectionProvider->hasFunction($nameNode, $scope); - } - - public function getFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): FunctionReflection - { - return $this->reflectionProvider->getFunction($nameNode, $scope); - } - - public function resolveFunctionName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string - { - return $this->reflectionProvider->resolveFunctionName($nameNode, $scope); - } - - public function hasConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool - { - return $this->reflectionProvider->hasConstant($nameNode, $scope); - } - - public function getConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection - { - return $this->reflectionProvider->getConstant($nameNode, $scope); - } - - public function resolveConstantName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string - { - return $this->reflectionProvider->resolveConstantName($nameNode, $scope); - } - /** - * @return string[] + * @deprecated Use PHPStan\Reflection\ReflectionProvider instead */ - public function getUniversalObjectCratesClasses(): array + public function hasFunction(Node\Name $nameNode, ?Scope $scope): bool { - return $this->universalObjectCratesClasses; + return $this->reflectionProvider->hasFunction($nameNode, $scope); } /** - * @param string $className - * @return \PHPStan\Type\DynamicMethodReturnTypeExtension[] + * @deprecated Use PHPStan\Reflection\ReflectionProvider instead */ - public function getDynamicMethodReturnTypeExtensionsForClass(string $className): array + public function getFunction(Node\Name $nameNode, ?Scope $scope): FunctionReflection { - return $this->dynamicReturnTypeExtensionRegistryProvider->getRegistry()->getDynamicMethodReturnTypeExtensionsForClass($className); + return $this->reflectionProvider->getFunction($nameNode, $scope); } /** - * @param string $className - * @return \PHPStan\Type\DynamicStaticMethodReturnTypeExtension[] + * @deprecated Use PHPStan\Reflection\ReflectionProvider instead */ - public function getDynamicStaticMethodReturnTypeExtensionsForClass(string $className): array + public function resolveFunctionName(Node\Name $nameNode, ?Scope $scope): ?string { - return $this->dynamicReturnTypeExtensionRegistryProvider->getRegistry()->getDynamicStaticMethodReturnTypeExtensionsForClass($className); + return $this->reflectionProvider->resolveFunctionName($nameNode, $scope); } /** - * @return OperatorTypeSpecifyingExtension[] + * @deprecated Use PHPStan\Reflection\ReflectionProvider instead */ - public function getOperatorTypeSpecifyingExtensions(string $operator, Type $leftType, Type $rightType): array + public function hasConstant(Node\Name $nameNode, ?Scope $scope): bool { - return $this->operatorTypeSpecifyingExtensionRegistryProvider->getRegistry()->getOperatorTypeSpecifyingExtensions($operator, $leftType, $rightType); + return $this->reflectionProvider->hasConstant($nameNode, $scope); } /** - * @return \PHPStan\Type\DynamicFunctionReturnTypeExtension[] + * @deprecated Use PHPStan\Reflection\ReflectionProvider instead */ - public function getDynamicFunctionReturnTypeExtensions(): array + public function getConstant(Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection { - return $this->dynamicReturnTypeExtensionRegistryProvider->getRegistry()->getDynamicFunctionReturnTypeExtensions(); + return $this->reflectionProvider->getConstant($nameNode, $scope); } /** - * @internal - * @return DynamicReturnTypeExtensionRegistryProvider + * @deprecated Use PHPStan\Reflection\ReflectionProvider instead */ - public function getDynamicReturnTypeExtensionRegistryProvider(): DynamicReturnTypeExtensionRegistryProvider + public function resolveConstantName(Node\Name $nameNode, ?Scope $scope): ?string { - return $this->dynamicReturnTypeExtensionRegistryProvider; + return $this->reflectionProvider->resolveConstantName($nameNode, $scope); } /** - * @internal - * @return \PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider + * @deprecated Inject %universalObjectCratesClasses% parameter instead. + * + * @return string[] */ - public function getOperatorTypeSpecifyingExtensionRegistryProvider(): OperatorTypeSpecifyingExtensionRegistryProvider + public function getUniversalObjectCratesClasses(): array { - return $this->operatorTypeSpecifyingExtensionRegistryProvider; + return $this->universalObjectCratesClasses; } } diff --git a/src/Broker/BrokerFactory.php b/src/Broker/BrokerFactory.php index 18eba1875c..182b0ca3ca 100644 --- a/src/Broker/BrokerFactory.php +++ b/src/Broker/BrokerFactory.php @@ -3,8 +3,6 @@ namespace PHPStan\Broker; use PHPStan\DependencyInjection\Container; -use PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider; -use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider; use PHPStan\Reflection\ReflectionProvider; class BrokerFactory @@ -17,20 +15,15 @@ class BrokerFactory public const DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG = 'phpstan.broker.dynamicFunctionReturnTypeExtension'; public const OPERATOR_TYPE_SPECIFYING_EXTENSION_TAG = 'phpstan.broker.operatorTypeSpecifyingExtension'; - private \PHPStan\DependencyInjection\Container $container; - - public function __construct(Container $container) + public function __construct(private Container $container) { - $this->container = $container; } public function create(): Broker { return new Broker( $this->container->getByType(ReflectionProvider::class), - $this->container->getByType(DynamicReturnTypeExtensionRegistryProvider::class), - $this->container->getByType(OperatorTypeSpecifyingExtensionRegistryProvider::class), - $this->container->getParameter('universalObjectCratesClasses') + $this->container->getParameter('universalObjectCratesClasses'), ); } diff --git a/src/Broker/ClassAutoloadingException.php b/src/Broker/ClassAutoloadingException.php index 9898953138..23e99fe5a5 100644 --- a/src/Broker/ClassAutoloadingException.php +++ b/src/Broker/ClassAutoloadingException.php @@ -2,14 +2,19 @@ namespace PHPStan\Broker; -class ClassAutoloadingException extends \PHPStan\AnalysedCodeException +use PHPStan\AnalysedCodeException; +use Throwable; +use function get_class; +use function sprintf; + +class ClassAutoloadingException extends AnalysedCodeException { private string $className; public function __construct( string $functionName, - ?\Throwable $previous = null + ?Throwable $previous = null, ) { if ($previous !== null) { @@ -17,12 +22,12 @@ public function __construct( '%s (%s) thrown while looking for class %s.', get_class($previous), $previous->getMessage(), - $functionName + $functionName, ), 0, $previous); } else { parent::__construct(sprintf( 'Class %s not found.', - $functionName + $functionName, ), 0); } diff --git a/src/Broker/ClassNotFoundException.php b/src/Broker/ClassNotFoundException.php index 1afb8d7458..d86a1bcb08 100644 --- a/src/Broker/ClassNotFoundException.php +++ b/src/Broker/ClassNotFoundException.php @@ -2,15 +2,15 @@ namespace PHPStan\Broker; -class ClassNotFoundException extends \PHPStan\AnalysedCodeException -{ +use PHPStan\AnalysedCodeException; +use function sprintf; - private string $className; +class ClassNotFoundException extends AnalysedCodeException +{ - public function __construct(string $functionName) + public function __construct(private string $className) { - parent::__construct(sprintf('Class %s was not found while trying to analyse it - discovering symbols is probably not configured properly.', $functionName)); - $this->className = $functionName; + parent::__construct(sprintf('Class %s was not found while trying to analyse it - discovering symbols is probably not configured properly.', $className)); } public function getClassName(): string diff --git a/src/Broker/ConstantNotFoundException.php b/src/Broker/ConstantNotFoundException.php index 25a3f7b775..2c490e3653 100644 --- a/src/Broker/ConstantNotFoundException.php +++ b/src/Broker/ConstantNotFoundException.php @@ -2,15 +2,15 @@ namespace PHPStan\Broker; -class ConstantNotFoundException extends \PHPStan\AnalysedCodeException -{ +use PHPStan\AnalysedCodeException; +use function sprintf; - private string $constantName; +class ConstantNotFoundException extends AnalysedCodeException +{ - public function __construct(string $constantName) + public function __construct(private string $constantName) { parent::__construct(sprintf('Constant %s not found.', $constantName)); - $this->constantName = $constantName; } public function getConstantName(): string diff --git a/src/Broker/FunctionNotFoundException.php b/src/Broker/FunctionNotFoundException.php index f3966b1ef8..de2728001f 100644 --- a/src/Broker/FunctionNotFoundException.php +++ b/src/Broker/FunctionNotFoundException.php @@ -2,15 +2,15 @@ namespace PHPStan\Broker; -class FunctionNotFoundException extends \PHPStan\AnalysedCodeException -{ +use PHPStan\AnalysedCodeException; +use function sprintf; - private string $functionName; +class FunctionNotFoundException extends AnalysedCodeException +{ - public function __construct(string $functionName) + public function __construct(private string $functionName) { parent::__construct(sprintf('Function %s not found while trying to analyse it - discovering symbols is probably not configured properly.', $functionName)); - $this->functionName = $functionName; } public function getFunctionName(): string diff --git a/src/Cache/Cache.php b/src/Cache/Cache.php index 1447662c58..0180ab6610 100644 --- a/src/Cache/Cache.php +++ b/src/Cache/Cache.php @@ -5,15 +5,11 @@ class Cache { - private \PHPStan\Cache\CacheStorage $storage; - - public function __construct(CacheStorage $storage) + public function __construct(private CacheStorage $storage) { - $this->storage = $storage; } /** - * @param string $key * @return mixed|null */ public function load(string $key, string $variableKey) @@ -22,10 +18,7 @@ public function load(string $key, string $variableKey) } /** - * @param string $key - * @param string $variableKey * @param mixed $data - * @return void */ public function save(string $key, string $variableKey, $data): void { diff --git a/src/Cache/CacheItem.php b/src/Cache/CacheItem.php index 17114f5fc5..61d9946c90 100644 --- a/src/Cache/CacheItem.php +++ b/src/Cache/CacheItem.php @@ -5,19 +5,11 @@ class CacheItem { - private string $variableKey; - - /** @var mixed */ - private $data; - /** - * @param string $variableKey * @param mixed $data */ - public function __construct(string $variableKey, $data) + public function __construct(private string $variableKey, private $data) { - $this->variableKey = $variableKey; - $this->data = $data; } public function isVariableKeyValid(string $variableKey): bool @@ -35,7 +27,6 @@ public function getData() /** * @param mixed[] $properties - * @return self */ public static function __set_state(array $properties): self { diff --git a/src/Cache/CacheStorage.php b/src/Cache/CacheStorage.php index a9227b0eca..c3a645eb2b 100644 --- a/src/Cache/CacheStorage.php +++ b/src/Cache/CacheStorage.php @@ -6,17 +6,12 @@ interface CacheStorage { /** - * @param string $key - * @param string $variableKey * @return mixed|null */ public function load(string $key, string $variableKey); /** - * @param string $key - * @param string $variableKey * @param mixed $data - * @return void */ public function save(string $key, string $variableKey, $data): void; diff --git a/src/Cache/FileCacheStorage.php b/src/Cache/FileCacheStorage.php index 1ef9950590..fbf0bb359e 100644 --- a/src/Cache/FileCacheStorage.php +++ b/src/Cache/FileCacheStorage.php @@ -2,17 +2,28 @@ namespace PHPStan\Cache; +use InvalidArgumentException; use Nette\Utils\Random; use PHPStan\File\FileWriter; +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; +use function substr; +use function unlink; +use function var_export; +use const DIRECTORY_SEPARATOR; class FileCacheStorage implements CacheStorage { - private string $directory; - - public function __construct(string $directory) + public function __construct(private string $directory) { - $this->directory = $directory; } private function makeDir(string $directory): void @@ -29,19 +40,18 @@ private function makeDir(string $directory): void } $error = error_get_last(); - throw new \InvalidArgumentException(sprintf('Failed to create directory "%s" (%s).', $this->directory, $error !== null ? $error['message'] : 'unknown cause')); + throw new InvalidArgumentException(sprintf('Failed to create directory "%s" (%s).', $this->directory, $error !== null ? $error['message'] : 'unknown cause')); } } /** - * @param string $key - * @param string $variableKey * @return mixed|null */ public function load(string $key, string $variableKey) { - return (function (string $key, string $variableKey) { - [,, $filePath] = $this->getFilePaths($key); + [,, $filePath] = $this->getFilePaths($key); + + return (static function () use ($variableKey, $filePath) { if (!is_file($filePath)) { return null; } @@ -55,14 +65,11 @@ public function load(string $key, string $variableKey) } return $cacheItem->getData(); - })($key, $variableKey); + })(); } /** - * @param string $key - * @param string $variableKey * @param mixed $data - * @return void */ public function save(string $key, string $variableKey, $data): void { @@ -76,14 +83,14 @@ public function save(string $key, string $variableKey, $data): void $exported = @var_export(new CacheItem($variableKey, $data), true); $errorAfter = error_get_last(); if ($errorAfter !== null && $errorBefore !== $errorAfter) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Error occurred while saving item %s (%s) to cache: %s', $key, $variableKey, $errorAfter['message'])); + throw new ShouldNotHappenException(sprintf('Error occurred while saving item %s (%s) to cache: %s', $key, $variableKey, $errorAfter['message'])); } FileWriter::write( $tmpPath, sprintf( " */ + /** @var array */ private array $storage = []; /** - * @param string $key - * @param string $variableKey * @return mixed|null */ public function load(string $key, string $variableKey) @@ -28,10 +26,7 @@ public function load(string $key, string $variableKey) } /** - * @param string $key - * @param string $variableKey * @param mixed $data - * @return void */ public function save(string $key, string $variableKey, $data): void { diff --git a/src/Command/AnalyseApplication.php b/src/Command/AnalyseApplication.php index 4d1656b0c6..73aab3de3e 100644 --- a/src/Command/AnalyseApplication.php +++ b/src/Command/AnalyseApplication.php @@ -7,50 +7,31 @@ use PHPStan\Analyser\ResultCache\ResultCacheManagerFactory; use PHPStan\Internal\BytesHelper; use PHPStan\PhpDoc\StubValidator; +use PHPStan\ShouldNotHappenException; use Symfony\Component\Console\Input\InputInterface; +use function array_merge; +use function count; +use function is_string; +use function memory_get_peak_usage; +use function microtime; +use function sprintf; class AnalyseApplication { - private AnalyserRunner $analyserRunner; - - private \PHPStan\PhpDoc\StubValidator $stubValidator; - - private \PHPStan\Analyser\ResultCache\ResultCacheManagerFactory $resultCacheManagerFactory; - - private IgnoredErrorHelper $ignoredErrorHelper; - - private string $memoryLimitFile; - - private int $internalErrorsCountLimit; - public function __construct( - AnalyserRunner $analyserRunner, - StubValidator $stubValidator, - ResultCacheManagerFactory $resultCacheManagerFactory, - IgnoredErrorHelper $ignoredErrorHelper, - string $memoryLimitFile, - int $internalErrorsCountLimit + private AnalyserRunner $analyserRunner, + private StubValidator $stubValidator, + private ResultCacheManagerFactory $resultCacheManagerFactory, + private IgnoredErrorHelper $ignoredErrorHelper, + private int $internalErrorsCountLimit, ) { - $this->analyserRunner = $analyserRunner; - $this->stubValidator = $stubValidator; - $this->resultCacheManagerFactory = $resultCacheManagerFactory; - $this->ignoredErrorHelper = $ignoredErrorHelper; - $this->memoryLimitFile = $memoryLimitFile; - $this->internalErrorsCountLimit = $internalErrorsCountLimit; } /** * @param string[] $files - * @param bool $onlyFiles - * @param \PHPStan\Command\Output $stdOutput - * @param \PHPStan\Command\Output $errorOutput - * @param bool $defaultLevelUsed - * @param bool $debug - * @param string|null $projectConfigFile * @param mixed[]|null $projectConfigArray - * @return AnalysisResult */ public function analyse( array $files, @@ -61,38 +42,14 @@ public function analyse( bool $debug, ?string $projectConfigFile, ?array $projectConfigArray, - InputInterface $input + InputInterface $input, ): AnalysisResult { - $this->updateMemoryLimitFile(); - $projectStubFiles = []; - if ($projectConfigArray !== null) { - $projectStubFiles = $projectConfigArray['parameters']['stubFiles'] ?? []; - } - $stubErrors = $this->stubValidator->validate($projectStubFiles, $debug); - - register_shutdown_function(function (): void { - $error = error_get_last(); - if ($error === null) { - return; - } - if ($error['type'] !== E_ERROR) { - return; - } - - if (strpos($error['message'], 'Allowed memory size') !== false) { - return; - } - - @unlink($this->memoryLimitFile); - }); - $resultCacheManager = $this->resultCacheManagerFactory->create([]); $ignoredErrorHelperResult = $this->ignoredErrorHelper->initialize(); if (count($ignoredErrorHelperResult->getErrors()) > 0) { $errors = $ignoredErrorHelperResult->getErrors(); - $warnings = []; $internalErrors = []; $savedResultCache = false; if ($errorOutput->isDebug()) { @@ -107,13 +64,28 @@ public function analyse( $projectConfigFile, $stdOutput, $errorOutput, - $input + $input, ); + + $projectStubFiles = []; + if ($projectConfigArray !== null) { + $projectStubFiles = $projectConfigArray['parameters']['stubFiles'] ?? []; + } + if ($resultCache->isFullAnalysis() && count($projectStubFiles) !== 0) { + $stubErrors = $this->stubValidator->validate($projectStubFiles, $debug); + $intermediateAnalyserResult = new AnalyserResult( + array_merge($intermediateAnalyserResult->getErrors(), $stubErrors), + $intermediateAnalyserResult->getInternalErrors(), + $intermediateAnalyserResult->getDependencies(), + $intermediateAnalyserResult->getExportedNodes(), + $intermediateAnalyserResult->hasReachedInternalErrorsCountLimit(), + ); + } + $resultCacheResult = $resultCacheManager->process($intermediateAnalyserResult, $resultCache, $errorOutput, $onlyFiles, true); $analyserResult = $resultCacheResult->getAnalyserResult(); $internalErrors = $analyserResult->getInternalErrors(); $errors = $ignoredErrorHelperResult->process($analyserResult->getErrors(), $onlyFiles, $files, count($internalErrors) > 0 || $analyserResult->hasReachedInternalErrorsCountLimit()); - $warnings = $ignoredErrorHelperResult->getWarnings(); $savedResultCache = $resultCacheResult->isSaved(); if ($analyserResult->hasReachedInternalErrorsCountLimit()) { $errors[] = sprintf('Reached internal errors count limit of %d, exiting...', $this->internalErrorsCountLimit); @@ -121,8 +93,6 @@ public function analyse( $errors = array_merge($errors, $internalErrors); } - $errors = array_merge($stubErrors, $errors); - $fileSpecificErrors = []; $notFileSpecificErrors = []; foreach ($errors as $error) { @@ -138,10 +108,10 @@ public function analyse( $fileSpecificErrors, $notFileSpecificErrors, $internalErrors, - $warnings, + [], $defaultLevelUsed, $projectConfigFile, - $savedResultCache + $savedResultCache, ); } @@ -156,7 +126,7 @@ private function runAnalyser( ?string $projectConfigFile, Output $stdOutput, Output $errorOutput, - InputInterface $input + InputInterface $input, ): AnalyserResult { $filesCount = count($files); @@ -170,32 +140,31 @@ private function runAnalyser( if (!$debug) { $progressStarted = false; - $fileOrder = 0; $preFileCallback = null; - $postFileCallback = function (int $step) use ($errorOutput, &$progressStarted, $allAnalysedFilesCount, $filesCount, &$fileOrder): void { + $postFileCallback = static function (int $step) use ($errorOutput, &$progressStarted, $allAnalysedFilesCount, $filesCount): void { if (!$progressStarted) { $errorOutput->getStyle()->progressStart($allAnalysedFilesCount); $errorOutput->getStyle()->progressAdvance($allAnalysedFilesCount - $filesCount); $progressStarted = true; } $errorOutput->getStyle()->progressAdvance($step); - - if ($fileOrder >= 100) { - $this->updateMemoryLimitFile(); - $fileOrder = 0; - } - $fileOrder += $step; }; } else { - $preFileCallback = static function (string $file) use ($stdOutput): void { + $startTime = null; + $preFileCallback = static function (string $file) use ($stdOutput, &$startTime): void { $stdOutput->writeLineFormatted($file); + $startTime = microtime(true); }; $postFileCallback = null; if ($stdOutput->isDebug()) { $previousMemory = memory_get_peak_usage(true); - $postFileCallback = static function () use ($stdOutput, &$previousMemory): void { + $postFileCallback = static function () use ($stdOutput, &$previousMemory, &$startTime): void { + if ($startTime === null) { + throw new ShouldNotHappenException(); + } $currentTotalMemory = memory_get_peak_usage(true); - $stdOutput->writeLineFormatted(sprintf('--- consumed %s, total %s', BytesHelper::bytes($currentTotalMemory - $previousMemory), BytesHelper::bytes($currentTotalMemory))); + $elapsedTime = microtime(true) - $startTime; + $stdOutput->writeLineFormatted(sprintf('--- consumed %s, total %s, took %.2f s', BytesHelper::bytes($currentTotalMemory - $previousMemory), BytesHelper::bytes($currentTotalMemory), $elapsedTime)); $previousMemory = $currentTotalMemory; }; } @@ -210,11 +179,4 @@ private function runAnalyser( return $analyserResult; } - private function updateMemoryLimitFile(): void - { - $bytes = memory_get_peak_usage(true); - $megabytes = ceil($bytes / 1024 / 1024); - file_put_contents($this->memoryLimitFile, sprintf('%d MB', $megabytes)); - } - } diff --git a/src/Command/AnalyseCommand.php b/src/Command/AnalyseCommand.php index 4a110d3bdc..cf441654c6 100644 --- a/src/Command/AnalyseCommand.php +++ b/src/Command/AnalyseCommand.php @@ -3,23 +3,48 @@ namespace PHPStan\Command; use OndraM\CiDetector\CiDetector; +use OndraM\CiDetector\Exception\CiNotDetectedException; use PHPStan\Analyser\ResultCache\ResultCacheClearer; use PHPStan\Command\ErrorFormatter\BaselineNeonErrorFormatter; use PHPStan\Command\ErrorFormatter\ErrorFormatter; use PHPStan\Command\ErrorFormatter\TableErrorFormatter; use PHPStan\Command\Symfony\SymfonyOutput; use PHPStan\Command\Symfony\SymfonyStyle; +use PHPStan\File\CouldNotWriteFileException; use PHPStan\File\FileWriter; use PHPStan\File\ParentDirectoryRelativePathHelper; +use PHPStan\File\PathNotFoundException; +use PHPStan\ShouldNotHappenException; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\StringInput; +use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\StreamOutput; +use Throwable; +use function array_map; +use function count; +use function dirname; +use function fopen; +use function get_class; +use function implode; +use function is_array; +use function is_bool; +use function is_dir; +use function is_string; +use function mkdir; +use function pathinfo; +use function rewind; +use function sprintf; use function stream_get_contents; +use function strlen; +use function substr; +use const PATHINFO_BASENAME; +use const PATHINFO_EXTENSION; -class AnalyseCommand extends \Symfony\Component\Console\Command\Command +class AnalyseCommand extends Command { private const NAME = 'analyse'; @@ -28,18 +53,14 @@ class AnalyseCommand extends \Symfony\Component\Console\Command\Command public const DEFAULT_LEVEL = CommandHelper::DEFAULT_LEVEL; - /** @var string[] */ - private array $composerAutoloaderProjectPaths; - /** * @param string[] $composerAutoloaderProjectPaths */ public function __construct( - array $composerAutoloaderProjectPaths + private array $composerAutoloaderProjectPaths, ) { parent::__construct(); - $this->composerAutoloaderProjectPaths = $composerAutoloaderProjectPaths; } protected function configure(): void @@ -48,14 +69,14 @@ protected function configure(): void ->setDescription('Analyses source code') ->setDefinition([ new InputArgument('paths', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Paths with source code to run analysis on'), - new InputOption('paths-file', null, InputOption::VALUE_REQUIRED, 'Path to a file with a list of paths to run analysis on'), new InputOption('configuration', 'c', InputOption::VALUE_REQUIRED, 'Path to project configuration file'), new InputOption(self::OPTION_LEVEL, 'l', InputOption::VALUE_REQUIRED, 'Level of rule options - the higher the stricter'), new InputOption(ErrorsConsoleStyle::OPTION_NO_PROGRESS, null, InputOption::VALUE_NONE, 'Do not show progress bar, only results'), new InputOption('debug', null, InputOption::VALUE_NONE, 'Show debug information - which file is analysed, do not catch internal errors'), new InputOption('autoload-file', 'a', InputOption::VALUE_REQUIRED, 'Project\'s additional autoload file path'), new InputOption('error-format', null, InputOption::VALUE_REQUIRED, 'Format in which to print the result of the analysis', null), - new InputOption('generate-baseline', null, InputOption::VALUE_OPTIONAL, 'Path to a file where the baseline should be saved', false), + new InputOption('generate-baseline', 'b', InputOption::VALUE_OPTIONAL, 'Path to a file where the baseline should be saved', false), + new InputOption('allow-empty-baseline', null, InputOption::VALUE_NONE, 'Do not error out when the generated baseline is empty'), new InputOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for analysis'), new InputOption('xdebug', null, InputOption::VALUE_NONE, 'Allow running with XDebug for debugging purposes'), new InputOption('fix', null, InputOption::VALUE_NONE, 'Launch PHPStan Pro'), @@ -77,7 +98,7 @@ protected function initialize(InputInterface $input, OutputInterface $output): v if ((bool) $input->getOption('debug')) { $application = $this->getApplication(); if ($application === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $application->setCatchExceptions(false); return; @@ -86,12 +107,23 @@ protected function initialize(InputInterface $input, OutputInterface $output): v protected function execute(InputInterface $input, OutputInterface $output): int { + if ($output instanceof ConsoleOutputInterface) { + $errorOutput = $output->getErrorOutput(); + $errorOutput->writeln(''); + $errorOutput->writeln("⚠️ You're running a dev-master alias of phpstan/phpstan.️"); + $errorOutput->writeln(''); + $errorOutput->writeln('This alias is no longer updated. It\'s recommended to switch'); + $errorOutput->writeln('to stable releases by using the caret ^1.4 version range.'); + $errorOutput->writeln(''); + $errorOutput->writeln('See latest releases at:'); + $errorOutput->writeln('https://github.com/phpstan/phpstan/releases'); + $errorOutput->writeln(''); + } $paths = $input->getArgument('paths'); $memoryLimit = $input->getOption('memory-limit'); $autoloadFile = $input->getOption('autoload-file'); $configuration = $input->getOption('configuration'); $level = $input->getOption(self::OPTION_LEVEL); - $pathsFile = $input->getOption('paths-file'); $allowXdebug = $input->getOption('xdebug'); $debugEnabled = (bool) $input->getOption('debug'); $fix = (bool) $input->getOption('fix') || (bool) $input->getOption('watch') || (bool) $input->getOption('pro'); @@ -104,16 +136,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int $generateBaselineFile = 'phpstan-baseline.neon'; } + $allowEmptyBaseline = (bool) $input->getOption('allow-empty-baseline'); + if ( !is_array($paths) || (!is_string($memoryLimit) && $memoryLimit !== null) || (!is_string($autoloadFile) && $autoloadFile !== null) || (!is_string($configuration) && $configuration !== null) || (!is_string($level) && $level !== null) - || (!is_string($pathsFile) && $pathsFile !== null) || (!is_bool($allowXdebug)) ) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } try { @@ -121,7 +154,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $input, $output, $paths, - $pathsFile, $memoryLimit, $autoloadFile, $this->composerAutoloaderProjectPaths, @@ -129,13 +161,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int $generateBaselineFile, $level, $allowXdebug, - true, - $debugEnabled + $debugEnabled, ); - } catch (\PHPStan\Command\InceptionNotSuccessfulException $e) { + } catch (InceptionNotSuccessfulException $e) { return 1; } + if ($generateBaselineFile === null && $allowEmptyBaseline) { + $inceptionResult->getStdOutput()->getStyle()->error('You must pass the --generate-baseline option alongside --allow-empty-baseline.'); + return $inceptionResult->handleReturn(1); + } + $errorOutput = $inceptionResult->getErrorOutput(); $obsoleteDockerImage = $_SERVER['PHPSTAN_OBSOLETE_DOCKER_IMAGE'] ?? 'false'; if ($obsoleteDockerImage === 'true') { @@ -149,7 +185,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $errorFormat = $input->getOption('error-format'); if (!is_string($errorFormat) && $errorFormat !== null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ($errorFormat === null) { @@ -163,7 +199,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } elseif ($ci->getCiName() === CiDetector::CI_TEAMCITY) { $errorFormat = 'teamcity'; } - } catch (\OndraM\CiDetector\Exception\CiNotDetectedException $e) { + } catch (CiNotDetectedException) { // pass } } @@ -174,26 +210,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $errorOutput->writeLineFormatted(sprintf( 'Error formatter "%s" not found. Available error formatters are: %s', $errorFormat, - implode(', ', array_map(static function (string $name): string { - return substr($name, strlen('errorFormatter.')); - }, $container->findServiceNamesByType(ErrorFormatter::class))) + implode(', ', array_map(static fn (string $name): string => substr($name, strlen('errorFormatter.')), $container->findServiceNamesByType(ErrorFormatter::class))), )); return 1; } - if ($errorFormat === 'baselineNeon') { - $errorOutput = $inceptionResult->getErrorOutput(); - $errorOutput->writeLineFormatted('⚠️ You\'re using an obsolete option --error-format baselineNeon. ⚠️️'); - $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted(' There\'s a new and much better option --generate-baseline. Here are the advantages:'); - $errorOutput->writeLineFormatted(' 1) The current baseline file does not have to be commented-out'); - $errorOutput->writeLineFormatted(' nor emptied when generating the new baseline. It\'s excluded automatically.'); - $errorOutput->writeLineFormatted(' 2) Output no longer has to be redirected to a file, PHPStan saves the baseline'); - $errorOutput->writeLineFormatted(' to a specified path (defaults to phpstan-baseline.neon).'); - $errorOutput->writeLineFormatted(' 3) Baseline contains correct relative paths if saved to a subdirectory.'); - $errorOutput->writeLineFormatted(''); - } - $generateBaselineFile = $inceptionResult->getGenerateBaselineFile(); if ($generateBaselineFile !== null) { $baselineExtension = pathinfo($generateBaselineFile, PATHINFO_EXTENSION); @@ -211,7 +232,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int try { [$files, $onlyFiles] = $inceptionResult->getFiles(); - } catch (\PHPStan\File\PathNotFoundException $e) { + } catch (PathNotFoundException $e) { $inceptionResult->getErrorOutput()->writeLineFormatted(sprintf('%s', $e->getMessage())); return 1; } @@ -221,24 +242,44 @@ protected function execute(InputInterface $input, OutputInterface $output): int $debug = $input->getOption('debug'); if (!is_bool($debug)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - $analysisResult = $application->analyse( - $files, - $onlyFiles, - $inceptionResult->getStdOutput(), - $inceptionResult->getErrorOutput(), - $inceptionResult->isDefaultLevelUsed(), - $debug, - $inceptionResult->getProjectConfigFile(), - $inceptionResult->getProjectConfigArray(), - $input - ); + try { + $analysisResult = $application->analyse( + $files, + $onlyFiles, + $inceptionResult->getStdOutput(), + $inceptionResult->getErrorOutput(), + $inceptionResult->isDefaultLevelUsed(), + $debug, + $inceptionResult->getProjectConfigFile(), + $inceptionResult->getProjectConfigArray(), + $input, + ); + } catch (Throwable $t) { + if ($debug) { + $inceptionResult->getStdOutput()->writeRaw(sprintf( + 'Uncaught %s: %s in %s:%d', + get_class($t), + $t->getMessage(), + $t->getFile(), + $t->getLine(), + )); + $inceptionResult->getStdOutput()->writeLineFormatted(''); + $inceptionResult->getStdOutput()->writeRaw($t->getTraceAsString()); + $inceptionResult->getStdOutput()->writeLineFormatted(''); + + return $inceptionResult->handleReturn(1); + } + + throw $t; + } if ($generateBaselineFile !== null) { - if (!$analysisResult->hasErrors()) { + if (!$allowEmptyBaseline && !$analysisResult->hasErrors()) { $inceptionResult->getStdOutput()->getStyle()->error('No errors were found during the analysis. Baseline could not be generated.'); + $inceptionResult->getStdOutput()->writeLineFormatted('To allow generating empty baselines, pass --allow-empty-baseline option.'); return $inceptionResult->handleReturn(1); } @@ -260,7 +301,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int rewind($stream); $baselineContents = stream_get_contents($stream); if ($baselineContents === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if (!is_dir($baselineFileDirectory)) { @@ -274,7 +315,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int try { FileWriter::write($generateBaselineFile, $baselineContents); - } catch (\PHPStan\File\CouldNotWriteFileException $e) { + } catch (CouldNotWriteFileException $e) { $inceptionResult->getStdOutput()->writeLineFormatted($e->getMessage()); return $inceptionResult->handleReturn(1); @@ -337,7 +378,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int [], $analysisResult->isDefaultLevelUsed(), $analysisResult->getProjectConfigFile(), - $analysisResult->isResultCacheSaved() + $analysisResult->isResultCacheSaved(), ); $stdOutput = $inceptionResult->getStdOutput(); @@ -380,7 +421,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return $inceptionResult->handleReturn(1); } - $inceptionResult->handleReturn(0); // delete memory limit file + $inceptionResult->handleReturn(0); /** @var FixerApplication $fixerApplication */ $fixerApplication = $container->getByType(FixerApplication::class); @@ -393,7 +434,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $analysisResult->getFileSpecificErrors(), $analysisResult->getNotFileSpecificErrors(), count($files), - $_SERVER['argv'][0] + $_SERVER['argv'][0], ); } @@ -401,7 +442,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $errorFormatter = $container->getService($errorFormatterServiceName); return $inceptionResult->handleReturn( - $errorFormatter->formatErrors($analysisResult, $inceptionResult->getStdOutput()) + $errorFormatter->formatErrors($analysisResult, $inceptionResult->getStdOutput()), ); } @@ -409,7 +450,7 @@ private function createStreamOutput(): StreamOutput { $resource = fopen('php://memory', 'w', false); if ($resource === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return new StreamOutput($resource); } diff --git a/src/Command/AnalyserRunner.php b/src/Command/AnalyserRunner.php index e52904d869..633b11e344 100644 --- a/src/Command/AnalyserRunner.php +++ b/src/Command/AnalyserRunner.php @@ -2,61 +2,47 @@ namespace PHPStan\Command; +use Closure; use PHPStan\Analyser\Analyser; use PHPStan\Analyser\AnalyserResult; use PHPStan\Parallel\ParallelAnalyser; 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 is_file; class AnalyserRunner { - private Scheduler $scheduler; - - private Analyser $analyser; - - private ParallelAnalyser $parallelAnalyser; - - private CpuCoreCounter $cpuCoreCounter; - public function __construct( - Scheduler $scheduler, - Analyser $analyser, - ParallelAnalyser $parallelAnalyser, - CpuCoreCounter $cpuCoreCounter + private Scheduler $scheduler, + private Analyser $analyser, + private ParallelAnalyser $parallelAnalyser, + private CpuCoreCounter $cpuCoreCounter, ) { - $this->scheduler = $scheduler; - $this->analyser = $analyser; - $this->parallelAnalyser = $parallelAnalyser; - $this->cpuCoreCounter = $cpuCoreCounter; } /** * @param string[] $files * @param string[] $allAnalysedFiles - * @param (\Closure(string $file): void)|null $preFileCallback - * @param (\Closure(int): void)|null $postFileCallback - * @param bool $debug - * @param bool $allowParallel - * @param string|null $projectConfigFile - * @param string|null $tmpFile - * @param string|null $insteadOfFile - * @param InputInterface $input - * @return AnalyserResult + * @param Closure(string $file): void|null $preFileCallback + * @param Closure(int ): void|null $postFileCallback */ public function runAnalyser( array $files, array $allAnalysedFiles, - ?\Closure $preFileCallback, - ?\Closure $postFileCallback, + ?Closure $preFileCallback, + ?Closure $postFileCallback, bool $debug, bool $allowParallel, ?string $projectConfigFile, ?string $tmpFile, ?string $insteadOfFile, - InputInterface $input + InputInterface $input, ): AnalyserResult { $filesCount = count($files); @@ -66,7 +52,7 @@ public function runAnalyser( $schedule = $this->scheduler->scheduleWork($this->cpuCoreCounter->getNumberOfCpuCores(), $files); $mainScript = null; - if (isset($_SERVER['argv'][0]) && file_exists($_SERVER['argv'][0])) { + if (isset($_SERVER['argv'][0]) && is_file($_SERVER['argv'][0])) { $mainScript = $_SERVER['argv'][0]; } @@ -74,7 +60,7 @@ public function runAnalyser( !$debug && $allowParallel && $mainScript !== null - && $schedule->getNumberOfProcesses() > 1 + && $schedule->getNumberOfProcesses() > 0 ) { return $this->parallelAnalyser->analyse($schedule, $mainScript, $postFileCallback, $projectConfigFile, $tmpFile, $insteadOfFile, $input); } @@ -84,20 +70,18 @@ public function runAnalyser( $preFileCallback, $postFileCallback, $debug, - $this->switchTmpFile($allAnalysedFiles, $insteadOfFile, $tmpFile) + $this->switchTmpFile($allAnalysedFiles, $insteadOfFile, $tmpFile), ); } /** * @param string[] $analysedFiles - * @param string|null $insteadOfFile - * @param string|null $tmpFile * @return string[] */ private function switchTmpFile( array $analysedFiles, ?string $insteadOfFile, - ?string $tmpFile + ?string $tmpFile, ): array { $analysedFiles = array_values(array_filter($analysedFiles, static function (string $file) use ($insteadOfFile): bool { diff --git a/src/Command/AnalysisResult.php b/src/Command/AnalysisResult.php index 2398b0833c..92cb217e21 100644 --- a/src/Command/AnalysisResult.php +++ b/src/Command/AnalysisResult.php @@ -3,69 +3,46 @@ namespace PHPStan\Command; use PHPStan\Analyser\Error; +use function count; +use function usort; +/** @api */ class AnalysisResult { - /** @var \PHPStan\Analyser\Error[] sorted by their file name, line number and message */ + /** @var Error[] sorted by their file name, line number and message */ private array $fileSpecificErrors; - /** @var string[] */ - private array $notFileSpecificErrors; - - /** @var string[] */ - private array $internalErrors; - - /** @var string[] */ - private array $warnings; - - private bool $defaultLevelUsed; - - private ?string $projectConfigFile; - - private bool $savedResultCache; - /** - * @param \PHPStan\Analyser\Error[] $fileSpecificErrors + * @param Error[] $fileSpecificErrors * @param string[] $notFileSpecificErrors * @param string[] $internalErrors * @param string[] $warnings - * @param bool $defaultLevelUsed - * @param string|null $projectConfigFile - * @param bool $savedResultCache */ public function __construct( array $fileSpecificErrors, - array $notFileSpecificErrors, - array $internalErrors, - array $warnings, - bool $defaultLevelUsed, - ?string $projectConfigFile, - bool $savedResultCache + private array $notFileSpecificErrors, + private array $internalErrors, + private array $warnings, + private bool $defaultLevelUsed, + private ?string $projectConfigFile, + private bool $savedResultCache, ) { usort( $fileSpecificErrors, - static function (Error $a, Error $b): int { - return [ - $a->getFile(), - $a->getLine(), - $a->getMessage(), - ] <=> [ - $b->getFile(), - $b->getLine(), - $b->getMessage(), - ]; - } + static fn (Error $a, Error $b): int => [ + $a->getFile(), + $a->getLine(), + $a->getMessage(), + ] <=> [ + $b->getFile(), + $b->getLine(), + $b->getMessage(), + ], ); $this->fileSpecificErrors = $fileSpecificErrors; - $this->notFileSpecificErrors = $notFileSpecificErrors; - $this->internalErrors = $internalErrors; - $this->warnings = $warnings; - $this->defaultLevelUsed = $defaultLevelUsed; - $this->projectConfigFile = $projectConfigFile; - $this->savedResultCache = $savedResultCache; } public function hasErrors(): bool @@ -79,7 +56,7 @@ public function getTotalErrorsCount(): int } /** - * @return \PHPStan\Analyser\Error[] sorted by their file name, line number and message + * @return Error[] sorted by their file name, line number and message */ public function getFileSpecificErrors(): array { diff --git a/src/Command/ClearResultCacheCommand.php b/src/Command/ClearResultCacheCommand.php index 6b5198c4dc..9730b74325 100644 --- a/src/Command/ClearResultCacheCommand.php +++ b/src/Command/ClearResultCacheCommand.php @@ -3,28 +3,26 @@ namespace PHPStan\Command; use PHPStan\Analyser\ResultCache\ResultCacheClearer; +use PHPStan\ShouldNotHappenException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use function is_string; class ClearResultCacheCommand extends Command { private const NAME = 'clear-result-cache'; - /** @var string[] */ - private array $composerAutoloaderProjectPaths; - /** * @param string[] $composerAutoloaderProjectPaths */ public function __construct( - array $composerAutoloaderProjectPaths + private array $composerAutoloaderProjectPaths, ) { parent::__construct(); - $this->composerAutoloaderProjectPaths = $composerAutoloaderProjectPaths; } protected function configure(): void @@ -34,6 +32,7 @@ protected function configure(): void ->setDefinition([ new InputOption('configuration', 'c', InputOption::VALUE_REQUIRED, 'Path to project configuration file'), 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 clearing result cache'), ]); } @@ -41,12 +40,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $autoloadFile = $input->getOption('autoload-file'); $configuration = $input->getOption('configuration'); + $memoryLimit = $input->getOption('memory-limit'); if ( (!is_string($autoloadFile) && $autoloadFile !== null) || (!is_string($configuration) && $configuration !== null) + || (!is_string($memoryLimit) && $memoryLimit !== null) ) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } try { @@ -54,17 +55,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int $input, $output, ['.'], - null, - null, + $memoryLimit, $autoloadFile, $this->composerAutoloaderProjectPaths, $configuration, null, '0', false, - false ); - } catch (\PHPStan\Command\InceptionNotSuccessfulException $e) { + } catch (InceptionNotSuccessfulException) { return 1; } diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index 5194ab3f3a..c3de7b2e84 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -2,42 +2,85 @@ namespace PHPStan\Command; +use Closure; use Composer\XdebugHandler\XdebugHandler; use Nette\DI\Config\Adapters\PhpAdapter; use Nette\DI\Helpers; -use Nette\Schema\Context as SchemaContext; -use Nette\Schema\Processor; +use Nette\DI\InvalidConfigurationException; +use Nette\DI\ServiceCreationException; +use Nette\FileNotFoundException; +use Nette\InvalidStateException; +use Nette\Schema\ValidationException; +use Nette\Utils\AssertionException; use Nette\Utils\Strings; use Nette\Utils\Validators; use PHPStan\Command\Symfony\SymfonyOutput; use PHPStan\Command\Symfony\SymfonyStyle; use PHPStan\DependencyInjection\Container; use PHPStan\DependencyInjection\ContainerFactory; +use PHPStan\DependencyInjection\InvalidIgnoredErrorPatternsException; use PHPStan\DependencyInjection\LoaderFactory; use PHPStan\DependencyInjection\NeonAdapter; +use PHPStan\ExtensionInstaller\GeneratedConfig; use PHPStan\File\FileFinder; use PHPStan\File\FileHelper; -use PHPStan\File\FileReader; +use PHPStan\ShouldNotHappenException; +use ReflectionClass; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Throwable; +use function array_diff_key; +use function array_key_exists; +use function array_map; +use function array_merge; +use function array_unique; +use function class_exists; +use function count; +use function dirname; +use function error_get_last; +use function function_exists; +use function get_class; +use function getcwd; +use function gettype; +use function implode; +use function ini_get; +use function ini_set; +use function is_dir; +use function is_file; +use function is_readable; +use function is_string; +use function mkdir; +use function pcntl_async_signals; +use function pcntl_signal; +use function register_shutdown_function; +use function sprintf; +use function str_ends_with; +use function str_repeat; +use function strpos; +use function sys_get_temp_dir; +use const DIRECTORY_SEPARATOR; +use const E_ERROR; +use const PHP_VERSION_ID; +use const SIGINT; class CommandHelper { public const DEFAULT_LEVEL = '0'; + private static ?string $reservedMemory = null; + /** * @param string[] $paths * @param string[] $composerAutoloaderProjectPaths * - * @throws \PHPStan\Command\InceptionNotSuccessfulException + * @throws InceptionNotSuccessfulException */ public static function begin( InputInterface $input, OutputInterface $output, array $paths, - ?string $pathsFile, ?string $memoryLimit, ?string $autoloadFile, array $composerAutoloaderProjectPaths, @@ -45,38 +88,60 @@ public static function begin( ?string $generateBaselineFile, ?string $level, bool $allowXdebug, - bool $manageMemoryLimitFile = true, bool $debugEnabled = false, ?string $singleReflectionFile = null, - ?string $singleReflectionInsteadOfFile = null + ?string $singleReflectionInsteadOfFile = null, + bool $cleanupContainerCache = true, ): InceptionResult { if (!$allowXdebug) { $xdebug = new XdebugHandler('phpstan'); + $xdebug->setPersistent(); $xdebug->check(); unset($xdebug); } + $stdOutput = new SymfonyOutput($output, new SymfonyStyle(new ErrorsConsoleStyle($input, $output))); - /** @var \PHPStan\Command\Output $errorOutput */ + /** @var Output $errorOutput */ $errorOutput = (static function () use ($input, $output): Output { $symfonyErrorOutput = $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output; return new SymfonyOutput($symfonyErrorOutput, new SymfonyStyle(new ErrorsConsoleStyle($input, $symfonyErrorOutput))); })(); if ($memoryLimit !== null) { - if (\Nette\Utils\Strings::match($memoryLimit, '#^-?\d+[kMG]?$#i') === null) { + if (Strings::match($memoryLimit, '#^-?\d+[kMG]?$#i') === null) { $errorOutput->writeLineFormatted(sprintf('Invalid memory limit format "%s".', $memoryLimit)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } if (ini_set('memory_limit', $memoryLimit) === false) { $errorOutput->writeLineFormatted(sprintf('Memory limit "%s" cannot be set.', $memoryLimit)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } } + self::$reservedMemory = str_repeat('PHPStan', 1463); // reserve 10 kB of space + register_shutdown_function(static function () use ($errorOutput): void { + self::$reservedMemory = null; + $error = error_get_last(); + if ($error === null) { + return; + } + if ($error['type'] !== E_ERROR) { + return; + } + + if (strpos($error['message'], 'Allowed memory size') === false) { + return; + } + + $errorOutput->writeLineFormatted(''); + $errorOutput->writeLineFormatted(sprintf('PHPStan process crashed because it reached configured PHP memory limit: %s', ini_get('memory_limit'))); + $errorOutput->writeLineFormatted('Increase your memory limit in php.ini or run PHPStan with --memory-limit CLI option.'); + }); + $currentWorkingDirectory = getcwd(); if ($currentWorkingDirectory === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $currentWorkingDirectoryFileHelper = new FileHelper($currentWorkingDirectory); $currentWorkingDirectory = $currentWorkingDirectoryFileHelper->getWorkingDirectory(); @@ -84,7 +149,7 @@ public static function begin( $autoloadFile = $currentWorkingDirectoryFileHelper->absolutizePath($autoloadFile); if (!is_file($autoloadFile)) { $errorOutput->writeLineFormatted(sprintf('Autoload file "%s" not found.', $autoloadFile)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } (static function (string $file): void { @@ -92,7 +157,7 @@ public static function begin( })($autoloadFile); } if ($projectConfigFile === null) { - foreach (['phpstan.neon', 'phpstan.neon.dist'] as $discoverableConfigName) { + foreach (['phpstan.neon', 'phpstan.neon.dist', 'phpstan.dist.neon'] as $discoverableConfigName) { $discoverableConfigFile = $currentWorkingDirectory . DIRECTORY_SEPARATOR . $discoverableConfigName; if (is_file($discoverableConfigFile)) { $projectConfigFile = $discoverableConfigFile; @@ -114,33 +179,7 @@ public static function begin( $defaultLevelUsed = true; } - $paths = array_map(static function (string $path) use ($currentWorkingDirectoryFileHelper): string { - return $currentWorkingDirectoryFileHelper->normalizePath($currentWorkingDirectoryFileHelper->absolutizePath($path)); - }, $paths); - - if (count($paths) === 0 && $pathsFile !== null) { - $pathsFile = $currentWorkingDirectoryFileHelper->absolutizePath($pathsFile); - if (!file_exists($pathsFile)) { - $errorOutput->writeLineFormatted(sprintf('Paths file %s does not exist.', $pathsFile)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); - } - - try { - $pathsString = FileReader::read($pathsFile); - } catch (\PHPStan\File\CouldNotReadFileException $e) { - $errorOutput->writeLineFormatted($e->getMessage()); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); - } - - $paths = array_values(array_filter(explode("\n", $pathsString), static function (string $path): bool { - return trim($path) !== ''; - })); - - $pathsFileFileHelper = new FileHelper(dirname($pathsFile)); - $paths = array_map(static function (string $path) use ($pathsFileFileHelper): string { - return $pathsFileFileHelper->normalizePath($pathsFileFileHelper->absolutizePath($path)); - }, $paths); - } + $paths = array_map(static fn (string $path): string => $currentWorkingDirectoryFileHelper->normalizePath($currentWorkingDirectoryFileHelper->absolutizePath($path)), $paths); $analysedPathsFromConfig = []; $containerFactory = new ContainerFactory($currentWorkingDirectory); @@ -148,21 +187,21 @@ public static function begin( if ($projectConfigFile !== null) { if (!is_file($projectConfigFile)) { $errorOutput->writeLineFormatted(sprintf('Project config file at path %s does not exist.', $projectConfigFile)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } $loader = (new LoaderFactory( $currentWorkingDirectoryFileHelper, $containerFactory->getRootDirectory(), $containerFactory->getCurrentWorkingDirectory(), - $generateBaselineFile + $generateBaselineFile, ))->createLoader(); try { $projectConfig = $loader->load($projectConfigFile, null); - } catch (\Nette\InvalidStateException | \Nette\FileNotFoundException $e) { + } catch (InvalidStateException | FileNotFoundException $e) { $errorOutput->writeLineFormatted($e->getMessage()); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } $defaultParameters = [ 'rootDir' => $containerFactory->getRootDirectory(), @@ -175,8 +214,10 @@ public static function begin( if ($level === null && isset($projectConfig['parameters']['level'])) { $level = (string) $projectConfig['parameters']['level']; } - if (count($paths) === 0 && isset($projectConfig['parameters']['paths'])) { + if (isset($projectConfig['parameters']['paths'])) { $analysedPathsFromConfig = Helpers::expand($projectConfig['parameters']['paths'], $defaultParameters); + } + if (count($paths) === 0) { $paths = $analysedPathsFromConfig; } } @@ -186,25 +227,25 @@ public static function begin( $levelConfigFile = sprintf('%s/config.level%s.neon', $containerFactory->getConfigDirectory(), $level); if (!is_file($levelConfigFile)) { $errorOutput->writeLineFormatted(sprintf('Level config file %s was not found.', $levelConfigFile)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } $additionalConfigFiles[] = $levelConfigFile; } if (class_exists('PHPStan\ExtensionInstaller\GeneratedConfig')) { - $generatedConfigReflection = new \ReflectionClass('PHPStan\ExtensionInstaller\GeneratedConfig'); + $generatedConfigReflection = new ReflectionClass('PHPStan\ExtensionInstaller\GeneratedConfig'); $generatedConfigDirectory = dirname($generatedConfigReflection->getFileName()); - foreach (\PHPStan\ExtensionInstaller\GeneratedConfig::EXTENSIONS as $name => $extensionConfig) { + foreach (GeneratedConfig::EXTENSIONS as $name => $extensionConfig) { foreach ($extensionConfig['extra']['includes'] ?? [] as $includedFile) { if (!is_string($includedFile)) { $errorOutput->writeLineFormatted(sprintf('Cannot include config from package %s, expecting string file path but got %s', $name, gettype($includedFile))); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } $includedFilePath = null; if (isset($extensionConfig['relative_install_path'])) { $includedFilePath = sprintf('%s/%s/%s', $generatedConfigDirectory, $extensionConfig['relative_install_path'], $includedFile); - if (!file_exists($includedFilePath) || !is_readable($includedFilePath)) { + if (!is_file($includedFilePath) || !is_readable($includedFilePath)) { $includedFilePath = null; } } @@ -212,16 +253,19 @@ public static function begin( if ($includedFilePath === null) { $includedFilePath = sprintf('%s/%s', $extensionConfig['install_path'], $includedFile); } - if (!file_exists($includedFilePath) || !is_readable($includedFilePath)) { + if (!is_file($includedFilePath) || !is_readable($includedFilePath)) { $errorOutput->writeLineFormatted(sprintf('Config file %s does not exist or isn\'t readable', $includedFilePath)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } $additionalConfigFiles[] = $includedFilePath; } } } - if ($projectConfigFile !== null) { + if ( + $projectConfigFile !== null + && $currentWorkingDirectoryFileHelper->normalizePath($projectConfigFile, '/') !== $currentWorkingDirectoryFileHelper->normalizePath(__DIR__ . '/../../conf/config.stubFiles.neon', '/') + ) { $additionalConfigFiles[] = $projectConfigFile; } @@ -234,13 +278,13 @@ public static function begin( $errorOutput, $currentWorkingDirectoryFileHelper, $additionalConfigFiles, - $loaderParameters + $loaderParameters, ); $createDir = static function (string $path) use ($errorOutput): void { - if (!@mkdir($path, 0777) && !is_dir($path)) { + if (!is_dir($path) && !@mkdir($path, 0777) && !is_dir($path)) { $errorOutput->writeLineFormatted(sprintf('Cannot create a temp directory %s', $path)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } }; @@ -251,32 +295,69 @@ public static function begin( try { $container = $containerFactory->create($tmpDir, $additionalConfigFiles, $paths, $composerAutoloaderProjectPaths, $analysedPathsFromConfig, $level ?? self::DEFAULT_LEVEL, $generateBaselineFile, $autoloadFile, $singleReflectionFile, $singleReflectionInsteadOfFile); - } catch (\Nette\DI\InvalidConfigurationException | \Nette\Utils\AssertionException $e) { + } catch (InvalidConfigurationException | AssertionException $e) { $errorOutput->writeLineFormatted('Invalid configuration:'); $errorOutput->writeLineFormatted($e->getMessage()); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); - } + throw new InceptionNotSuccessfulException(); + } catch (InvalidIgnoredErrorPatternsException $e) { + $errorOutput->writeLineFormatted(sprintf('Invalid %s in ignoreErrors:', count($e->getErrors()) === 1 ? 'entry' : 'entries')); + foreach ($e->getErrors() as $error) { + $errorOutput->writeLineFormatted($error); + $errorOutput->writeLineFormatted(''); + } + throw new InceptionNotSuccessfulException(); + } catch (ValidationException $e) { + foreach ($e->getMessages() as $message) { + $errorOutput->writeLineFormatted('Invalid configuration:'); + $errorOutput->writeLineFormatted($message); + } + throw new InceptionNotSuccessfulException(); + } catch (ServiceCreationException $e) { + $matches = Strings::match($e->getMessage(), '#Service of type (?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\\\\]*[a-zA-Z0-9_\x7f-\xff]): Service of type (?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\\\\]*[a-zA-Z0-9_\x7f-\xff]) needed by \$(?[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*) in (?[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*)\(\)#'); + if ($matches === null) { + throw $e; + } - $containerFactory->clearOldContainers($tmpDir); + if ($matches['parserServiceType'] !== 'PHPStan\\Parser\\Parser') { + throw $e; + } - if (count($paths) === 0) { - $errorOutput->writeLineFormatted('At least one path must be specified to analyse.'); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); - } + if ($matches['methodName'] !== '__construct') { + throw $e; + } + + $errorOutput->writeLineFormatted('Invalid configuration:'); + $errorOutput->writeLineFormatted(sprintf("Service of type %s is no longer autowired.\n", $matches['parserServiceType'])); + $errorOutput->writeLineFormatted('You need to choose one of the following services'); + $errorOutput->writeLineFormatted(sprintf('and use it in the %s argument of your service %s:', $matches['parameterName'], $matches['serviceType'])); + $errorOutput->writeLineFormatted('* defaultAnalysisParser (if you\'re parsing files from analysed paths)'); + $errorOutput->writeLineFormatted('* currentPhpVersionSimpleDirectParser (in most other situations)'); - $memoryLimitFile = $container->getParameter('memoryLimitFile'); - if ($manageMemoryLimitFile && file_exists($memoryLimitFile)) { - $memoryLimitFileContents = FileReader::read($memoryLimitFile); - $errorOutput->writeLineFormatted('PHPStan crashed in the previous run probably because of excessive memory consumption.'); - $errorOutput->writeLineFormatted(sprintf('It consumed around %s of memory.', $memoryLimitFileContents)); $errorOutput->writeLineFormatted(''); + $errorOutput->writeLineFormatted('After fixing this problem, your configuration will look something like this:'); + $errorOutput->writeLineFormatted(''); + $errorOutput->writeLineFormatted('-'); + $errorOutput->writeLineFormatted(sprintf("\tclass: %s", $matches['serviceType'])); + $errorOutput->writeLineFormatted(sprintf("\targuments:")); + $errorOutput->writeLineFormatted(sprintf("\t\t%s: @defaultAnalysisParser", $matches['parameterName'])); $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted('To avoid this issue, allow to use more memory with the --memory-limit option.'); - @unlink($memoryLimitFile); + + throw new InceptionNotSuccessfulException(); } - self::setUpSignalHandler($errorOutput, $manageMemoryLimitFile ? $memoryLimitFile : null); - if (!$container->hasParameter('customRulesetUsed')) { + if ($cleanupContainerCache) { + $containerFactory->clearOldContainers($tmpDir); + } + + if (count($paths) === 0) { + $errorOutput->writeLineFormatted('At least one path must be specified to analyse.'); + throw new InceptionNotSuccessfulException(); + } + + self::setUpSignalHandler($errorOutput); + /** @var bool|null $customRulesetUsed */ + $customRulesetUsed = $container->getParameter('customRulesetUsed'); + if ($customRulesetUsed === null) { $errorOutput->writeLineFormatted(''); $errorOutput->writeLineFormatted('No rules detected'); $errorOutput->writeLineFormatted(''); @@ -287,87 +368,23 @@ public static function begin( $errorOutput->writeLineFormatted(sprintf('* create your own custom ruleset by selecting which rules you want to check by copying the service definitions from the built-in config level files in %s.', $currentWorkingDirectoryFileHelper->normalizePath(__DIR__ . '/../../conf'))); $errorOutput->writeLineFormatted(' * in this case, don\'t forget to define parameter customRulesetUsed in your config file.'); $errorOutput->writeLineFormatted(''); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); - } elseif ((bool) $container->getParameter('customRulesetUsed')) { + throw new InceptionNotSuccessfulException(); + } elseif ($customRulesetUsed) { $defaultLevelUsed = false; } - $schema = $container->getParameter('__parametersSchema'); - $processor = new Processor(); - $processor->onNewContext[] = static function (SchemaContext $context): void { - $context->path = ['parameters']; - }; - - try { - $processor->process($schema, $container->getParameters()); - } catch (\Nette\Schema\ValidationException $e) { - foreach ($e->getMessages() as $message) { - $errorOutput->writeLineFormatted('Invalid configuration:'); - $errorOutput->writeLineFormatted($message); - } - throw new \PHPStan\Command\InceptionNotSuccessfulException(); - } - - $autoloadFiles = $container->getParameter('autoload_files'); - if ($manageMemoryLimitFile && count($autoloadFiles) > 0) { - $errorOutput->writeLineFormatted('⚠️ You\'re using a deprecated config option autoload_files. ⚠️️'); - $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted('You might not need it anymore - try removing it from your'); - $errorOutput->writeLineFormatted('configuration file and run PHPStan again.'); - $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted('If the analysis fails, there are now two distinct options'); - $errorOutput->writeLineFormatted('to choose from to replace autoload_files:'); - $errorOutput->writeLineFormatted('1) scanFiles - PHPStan will scan those for classes and functions'); - $errorOutput->writeLineFormatted(' definitions. PHPStan will not execute those files.'); - $errorOutput->writeLineFormatted('2) bootstrapFiles - PHPStan will execute these files to prepare'); - $errorOutput->writeLineFormatted(' the PHP runtime environment for the analysis.'); - $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted('Read more about this in PHPStan\'s documentation:'); - $errorOutput->writeLineFormatted('https://phpstan.org/user-guide/discovering-symbols'); - $errorOutput->writeLineFormatted(''); - } - - $autoloadDirectories = $container->getParameter('autoload_directories'); - if (count($autoloadDirectories) > 0 && $manageMemoryLimitFile) { - $errorOutput->writeLineFormatted('⚠️ You\'re using a deprecated config option autoload_directories. ⚠️️'); - $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted('You might not need it anymore - try removing it from your'); - $errorOutput->writeLineFormatted('configuration file and run PHPStan again.'); - $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted('If the analysis fails, replace it with scanDirectories.'); - $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted('Read more about this in PHPStan\'s documentation:'); - $errorOutput->writeLineFormatted('https://phpstan.org/user-guide/discovering-symbols'); - $errorOutput->writeLineFormatted(''); - } - - foreach ($autoloadFiles as $parameterAutoloadFile) { - if (!file_exists($parameterAutoloadFile)) { - $errorOutput->writeLineFormatted(sprintf('Autoload file %s does not exist.', $parameterAutoloadFile)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); - } - (static function (string $file) use ($container): void { - require_once $file; - })($parameterAutoloadFile); - } - - $bootstrapFile = $container->getParameter('bootstrap'); - if ($bootstrapFile !== null) { - if ($manageMemoryLimitFile) { - $errorOutput->writeLineFormatted('⚠️ You\'re using a deprecated config option bootstrap. ⚠️️'); - $errorOutput->writeLineFormatted(''); - $errorOutput->writeLineFormatted('This option has been replaced with bootstrapFiles which accepts a list of files'); - $errorOutput->writeLineFormatted('to execute before the analysis.'); - $errorOutput->writeLineFormatted(''); - } - - self::executeBootstrapFile($bootstrapFile, $container, $errorOutput, $debugEnabled); - } - foreach ($container->getParameter('bootstrapFiles') as $bootstrapFileFromArray) { self::executeBootstrapFile($bootstrapFileFromArray, $container, $errorOutput, $debugEnabled); } + if (PHP_VERSION_ID >= 80000) { + require_once __DIR__ . '/../../stubs/runtime/Enum/UnitEnum.php'; + require_once __DIR__ . '/../../stubs/runtime/Enum/BackedEnum.php'; + require_once __DIR__ . '/../../stubs/runtime/Enum/ReflectionEnum.php'; + require_once __DIR__ . '/../../stubs/runtime/Enum/ReflectionEnumUnitCase.php'; + require_once __DIR__ . '/../../stubs/runtime/Enum/ReflectionEnumBackedCase.php'; + } + foreach ($container->getParameter('scanFiles') as $scannedFile) { if (is_file($scannedFile)) { continue; @@ -375,7 +392,7 @@ public static function begin( $errorOutput->writeLineFormatted(sprintf('Scanned file %s does not exist.', $scannedFile)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } foreach ($container->getParameter('scanDirectories') as $scannedDirectory) { @@ -385,18 +402,15 @@ public static function begin( $errorOutput->writeLineFormatted(sprintf('Scanned directory %s does not exist.', $scannedDirectory)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } $alreadyAddedStubFiles = []; foreach ($container->getParameter('stubFiles') as $stubFile) { - if ( - $container->getParameter('featureToggles')['detectDuplicateStubFiles'] - && array_key_exists($stubFile, $alreadyAddedStubFiles) - ) { + if (array_key_exists($stubFile, $alreadyAddedStubFiles)) { $errorOutput->writeLineFormatted(sprintf('Stub file %s is added multiple times.', $stubFile)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } $alreadyAddedStubFiles[$stubFile] = true; @@ -407,7 +421,7 @@ public static function begin( $errorOutput->writeLineFormatted(sprintf('Stub file %s does not exist.', $stubFile)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } $excludesAnalyse = $container->getParameter('excludes_analyse'); @@ -418,7 +432,11 @@ public static function begin( $errorOutput->writeLineFormatted(sprintf('Parameter excludes_analyse has been deprecated so use excludePaths only from now on.')); $errorOutput->writeLineFormatted(''); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); + } elseif (count($excludesAnalyse) > 0) { + $errorOutput->writeLineFormatted('⚠️ You\'re using a deprecated config option excludes_analyse. ⚠️️'); + $errorOutput->writeLineFormatted(''); + $errorOutput->writeLineFormatted(sprintf('Parameter excludes_analyse has been deprecated so use excludePaths only from now on.')); } $tempResultCachePath = $container->getParameter('tempResultCachePath'); @@ -427,11 +445,16 @@ public static function begin( /** @var FileFinder $fileFinder */ $fileFinder = $container->getService('fileFinderAnalyse'); - /** @var \Closure(): (array{string[], bool}) $filesCallback */ - $filesCallback = static function () use ($fileFinder, $paths): array { + $pathRoutingParser = $container->getService('pathRoutingParser'); + + /** @var Closure(): array{string[], bool} $filesCallback */ + $filesCallback = static function () use ($fileFinder, $pathRoutingParser, $paths): array { $fileFinderResult = $fileFinder->findFiles($paths); + $files = $fileFinderResult->getFiles(); - return [$fileFinderResult->getFiles(), $fileFinderResult->isOnlyFiles()]; + $pathRoutingParser->setAnalysedFiles($files); + + return [$files, $fileFinderResult->isOnlyFiles()]; }; return new InceptionResult( @@ -440,10 +463,9 @@ public static function begin( $errorOutput, $container, $defaultLevelUsed, - $memoryLimitFile, $projectConfigFile, $projectConfig, - $generateBaselineFile + $generateBaselineFile, ); } @@ -454,56 +476,51 @@ private static function executeBootstrapFile( string $file, Container $container, Output $errorOutput, - bool $debugEnabled + bool $debugEnabled, ): void { if (!is_file($file)) { $errorOutput->writeLineFormatted(sprintf('Bootstrap file %s does not exist.', $file)); - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } try { (static function (string $file) use ($container): void { require_once $file; })($file); - } catch (\Throwable $e) { + } catch (Throwable $e) { $errorOutput->writeLineFormatted(sprintf('%s thrown in %s on line %d while loading bootstrap file %s: %s', get_class($e), $e->getFile(), $e->getLine(), $file, $e->getMessage())); if ($debugEnabled) { $errorOutput->writeLineFormatted($e->getTraceAsString()); } - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } } - private static function setUpSignalHandler(Output $output, ?string $memoryLimitFile): void + private static function setUpSignalHandler(Output $output): void { if (!function_exists('pcntl_signal')) { return; } pcntl_async_signals(true); - pcntl_signal(SIGINT, static function () use ($output, $memoryLimitFile): void { - if ($memoryLimitFile !== null && file_exists($memoryLimitFile)) { - @unlink($memoryLimitFile); - } + pcntl_signal(SIGINT, static function () use ($output): void { $output->writeLineFormatted(''); exit(1); }); } /** - * @param \PHPStan\Command\Output $output - * @param \PHPStan\File\FileHelper $fileHelper * @param string[] $configFiles * @param array $loaderParameters - * @throws \PHPStan\Command\InceptionNotSuccessfulException + * @throws InceptionNotSuccessfulException */ private static function detectDuplicateIncludedFiles( Output $output, FileHelper $fileHelper, array $configFiles, - array $loaderParameters + array $loaderParameters, ): void { $neonAdapter = new NeonAdapter(); @@ -513,9 +530,7 @@ private static function detectDuplicateIncludedFiles( $allConfigFiles = array_merge($allConfigFiles, self::getConfigFiles($fileHelper, $neonAdapter, $phpAdapter, $configFile, $loaderParameters, null)); } - $normalized = array_map(static function (string $file) use ($fileHelper): string { - return $fileHelper->normalizePath($file); - }, $allConfigFiles); + $normalized = array_map(static fn (string $file): string => $fileHelper->normalizePath($file), $allConfigFiles); $deduplicated = array_unique($normalized); if (count($normalized) <= count($deduplicated)) { @@ -534,15 +549,11 @@ private static function detectDuplicateIncludedFiles( $output->writeLineFormatted(''); $output->writeLineFormatted('It can lead to unexpected results. If you\'re using phpstan/extension-installer, make sure you have removed corresponding neon files from your project config file.'); } - throw new \PHPStan\Command\InceptionNotSuccessfulException(); + throw new InceptionNotSuccessfulException(); } /** - * @param \PHPStan\DependencyInjection\NeonAdapter $neonAdapter - * @param \Nette\DI\Config\Adapters\PhpAdapter $phpAdapter - * @param string $configFile * @param array $loaderParameters - * @param string|null $generateBaselineFile * @return string[] */ private static function getConfigFiles( @@ -551,7 +562,7 @@ private static function getConfigFiles( PhpAdapter $phpAdapter, string $configFile, array $loaderParameters, - ?string $generateBaselineFile + ?string $generateBaselineFile, ): array { if ($generateBaselineFile === $fileHelper->normalizePath($configFile)) { @@ -561,7 +572,7 @@ private static function getConfigFiles( return []; } - if (Strings::endsWith($configFile, '.php')) { + if (str_ends_with($configFile, '.php')) { $data = $phpAdapter->load($configFile); } else { $data = $neonAdapter->load($configFile); diff --git a/src/Command/DumpDependenciesCommand.php b/src/Command/DumpDependenciesCommand.php deleted file mode 100644 index aa69b5900d..0000000000 --- a/src/Command/DumpDependenciesCommand.php +++ /dev/null @@ -1,130 +0,0 @@ -composerAutoloaderProjectPaths = $composerAutoloaderProjectPaths; - } - - protected function configure(): void - { - $this->setName(self::NAME) - ->setDescription('Dumps files dependency tree') - ->setDefinition([ - new InputArgument('paths', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Paths with source code to run dump on'), - new InputOption('paths-file', null, InputOption::VALUE_REQUIRED, 'Path to a file with a list of paths to run analysis on'), - new InputOption('configuration', 'c', InputOption::VALUE_REQUIRED, 'Path to project configuration file'), - new InputOption(ErrorsConsoleStyle::OPTION_NO_PROGRESS, null, InputOption::VALUE_NONE, 'Do not show progress bar, only results'), - 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 the run'), - new InputOption('analysed-paths', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Project-scope paths'), - new InputOption('xdebug', null, InputOption::VALUE_NONE, 'Allow running with XDebug for debugging purposes'), - ]); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - try { - /** @var string[] $paths */ - $paths = $input->getArgument('paths'); - - /** @var string|null $memoryLimit */ - $memoryLimit = $input->getOption('memory-limit'); - - /** @var string|null $autoloadFile */ - $autoloadFile = $input->getOption('autoload-file'); - - /** @var string|null $configurationFile */ - $configurationFile = $input->getOption('configuration'); - - /** @var string|null $pathsFile */ - $pathsFile = $input->getOption('paths-file'); - - /** @var bool $allowXdebug */ - $allowXdebug = $input->getOption('xdebug'); - - $inceptionResult = CommandHelper::begin( - $input, - $output, - $paths, - $pathsFile, - $memoryLimit, - $autoloadFile, - $this->composerAutoloaderProjectPaths, - $configurationFile, - null, - '0', // irrelevant but prevents an error when a config file is passed - $allowXdebug, - true - ); - } catch (\PHPStan\Command\InceptionNotSuccessfulException $e) { - return 1; - } - - try { - [$files] = $inceptionResult->getFiles(); - } catch (\PHPStan\File\PathNotFoundException $e) { - $inceptionResult->getErrorOutput()->writeLineFormatted(sprintf('%s', $e->getMessage())); - return 1; - } - - $stdOutput = $inceptionResult->getStdOutput(); - $stdOutputStyole = $stdOutput->getStyle(); - - /** @var DependencyDumper $dependencyDumper */ - $dependencyDumper = $inceptionResult->getContainer()->getByType(DependencyDumper::class); - - /** @var FileHelper $fileHelper */ - $fileHelper = $inceptionResult->getContainer()->getByType(FileHelper::class); - - /** @var string[] $analysedPaths */ - $analysedPaths = $input->getOption('analysed-paths'); - $analysedPaths = array_map(static function (string $path) use ($fileHelper): string { - return $fileHelper->absolutizePath($path); - }, $analysedPaths); - $dependencies = $dependencyDumper->dumpDependencies( - $files, - static function (int $count) use ($stdOutputStyole): void { - $stdOutputStyole->progressStart($count); - }, - static function () use ($stdOutputStyole): void { - $stdOutputStyole->progressAdvance(); - }, - count($analysedPaths) > 0 ? $analysedPaths : null - ); - $stdOutputStyole->progressFinish(); - - try { - $stdOutput->writeLineFormatted(Json::encode($dependencies, Json::PRETTY)); - } catch (\Nette\Utils\JsonException $e) { - $inceptionResult->getErrorOutput()->writeLineFormatted(sprintf('%s', $e->getMessage())); - return 1; - } - - return $inceptionResult->handleReturn(0); - } - -} diff --git a/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php b/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php index f819dd664f..244f978551 100644 --- a/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php +++ b/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php @@ -7,23 +7,20 @@ use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; use PHPStan\File\RelativePathHelper; -use const SORT_STRING; use function ksort; use function preg_quote; +use const SORT_STRING; class BaselineNeonErrorFormatter implements ErrorFormatter { - private \PHPStan\File\RelativePathHelper $relativePathHelper; - - public function __construct(RelativePathHelper $relativePathHelper) + public function __construct(private RelativePathHelper $relativePathHelper) { - $this->relativePathHelper = $relativePathHelper; } public function formatErrors( AnalysisResult $analysisResult, - Output $output + Output $output, ): int { if (!$analysisResult->hasErrors()) { @@ -40,7 +37,7 @@ public function formatErrors( if (!$fileSpecificError->canBeIgnored()) { continue; } - $fileErrors[$fileSpecificError->getFilePath()][] = $fileSpecificError->getMessage(); + $fileErrors[$this->relativePathHelper->getRelativePath($fileSpecificError->getFilePath())][] = $fileSpecificError->getMessage(); } ksort($fileErrors, SORT_STRING); @@ -61,7 +58,7 @@ public function formatErrors( $errorsToOutput[] = [ 'message' => Helpers::escape('#^' . preg_quote($message, '#') . '$#'), 'count' => $count, - 'path' => Helpers::escape($this->relativePathHelper->getRelativePath($file)), + 'path' => Helpers::escape($file), ]; } } diff --git a/src/Command/ErrorFormatter/CheckstyleErrorFormatter.php b/src/Command/ErrorFormatter/CheckstyleErrorFormatter.php index b2cbb6ade0..33eb65bb37 100644 --- a/src/Command/ErrorFormatter/CheckstyleErrorFormatter.php +++ b/src/Command/ErrorFormatter/CheckstyleErrorFormatter.php @@ -2,23 +2,26 @@ namespace PHPStan\Command\ErrorFormatter; +use PHPStan\Analyser\Error; use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; use PHPStan\File\RelativePathHelper; +use function count; +use function htmlspecialchars; +use function sprintf; +use const ENT_COMPAT; +use const ENT_XML1; class CheckstyleErrorFormatter implements ErrorFormatter { - private RelativePathHelper $relativePathHelper; - - public function __construct(RelativePathHelper $relativePathHelper) + public function __construct(private RelativePathHelper $relativePathHelper) { - $this->relativePathHelper = $relativePathHelper; } public function formatErrors( AnalysisResult $analysisResult, - Output $output + Output $output, ): int { $output->writeRaw(''); @@ -29,7 +32,7 @@ public function formatErrors( foreach ($this->groupByFile($analysisResult) as $relativeFilePath => $errors) { $output->writeRaw(sprintf( '', - $this->escape($relativeFilePath) + $this->escape($relativeFilePath), )); $output->writeLineFormatted(''); @@ -37,7 +40,7 @@ public function formatErrors( $output->writeRaw(sprintf( ' ', $this->escape((string) $error->getLine()), - $this->escape($error->getMessage()) + $this->escape($error->getMessage()), )); $output->writeLineFormatted(''); } @@ -82,8 +85,6 @@ public function formatErrors( /** * Escapes values for using in XML * - * @param string $string - * @return string */ private function escape(string $string): string { @@ -93,22 +94,21 @@ private function escape(string $string): string /** * Group errors by file * - * @param AnalysisResult $analysisResult - * @return array> Array that have as key the relative path of file - * and as value an array with occurred errors. + * @return array> Array that have as key the relative path of file + * and as value an array with occurred errors. */ private function groupByFile(AnalysisResult $analysisResult): array { $files = []; - /** @var \PHPStan\Analyser\Error $fileSpecificError */ + /** @var Error $fileSpecificError */ foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { $absolutePath = $fileSpecificError->getFilePath(); if ($fileSpecificError->getTraitFilePath() !== null) { $absolutePath = $fileSpecificError->getTraitFilePath(); } $relativeFilePath = $this->relativePathHelper->getRelativePath( - $absolutePath + $absolutePath, ); $files[$relativeFilePath][] = $fileSpecificError; diff --git a/src/Command/ErrorFormatter/ErrorFormatter.php b/src/Command/ErrorFormatter/ErrorFormatter.php index 3c2663efe7..cc4b48df3a 100644 --- a/src/Command/ErrorFormatter/ErrorFormatter.php +++ b/src/Command/ErrorFormatter/ErrorFormatter.php @@ -12,13 +12,11 @@ interface ErrorFormatter /** * Formats the errors and outputs them to the console. * - * @param \PHPStan\Command\AnalysisResult $analysisResult - * @param \PHPStan\Command\Output $output * @return int Error code. */ public function formatErrors( AnalysisResult $analysisResult, - Output $output + Output $output, ): int; } diff --git a/src/Command/ErrorFormatter/GithubErrorFormatter.php b/src/Command/ErrorFormatter/GithubErrorFormatter.php index 0743b26d3a..33ee0f3b8e 100644 --- a/src/Command/ErrorFormatter/GithubErrorFormatter.php +++ b/src/Command/ErrorFormatter/GithubErrorFormatter.php @@ -5,6 +5,10 @@ use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; use PHPStan\File\RelativePathHelper; +use function array_walk; +use function implode; +use function sprintf; +use function str_replace; /** * Allow errors to be reported in pull-requests diff when run in a GitHub Action @@ -13,22 +17,16 @@ class GithubErrorFormatter implements ErrorFormatter { - private RelativePathHelper $relativePathHelper; - - private TableErrorFormatter $tableErrorformatter; - public function __construct( - RelativePathHelper $relativePathHelper, - TableErrorFormatter $tableErrorformatter + private RelativePathHelper $relativePathHelper, + private ErrorFormatter $errorFormatter, ) { - $this->relativePathHelper = $relativePathHelper; - $this->tableErrorformatter = $tableErrorformatter; } public function formatErrors(AnalysisResult $analysisResult, Output $output): int { - $this->tableErrorformatter->formatErrors($analysisResult, $output); + $this->errorFormatter->formatErrors($analysisResult, $output); foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { $metas = [ diff --git a/src/Command/ErrorFormatter/GitlabErrorFormatter.php b/src/Command/ErrorFormatter/GitlabErrorFormatter.php index 16713dd779..6aa4b61bef 100644 --- a/src/Command/ErrorFormatter/GitlabErrorFormatter.php +++ b/src/Command/ErrorFormatter/GitlabErrorFormatter.php @@ -6,6 +6,8 @@ use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; use PHPStan\File\RelativePathHelper; +use function hash; +use function implode; /** * @see https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html#implementing-a-custom-tool @@ -13,11 +15,8 @@ class GitlabErrorFormatter implements ErrorFormatter { - private RelativePathHelper $relativePathHelper; - - public function __construct(RelativePathHelper $relativePathHelper) + public function __construct(private RelativePathHelper $relativePathHelper) { - $this->relativePathHelper = $relativePathHelper; } public function formatErrors(AnalysisResult $analysisResult, Output $output): int @@ -34,14 +33,14 @@ public function formatErrors(AnalysisResult $analysisResult, Output $output): in $fileSpecificError->getFile(), $fileSpecificError->getLine(), $fileSpecificError->getMessage(), - ] - ) + ], + ), ), 'severity' => $fileSpecificError->canBeIgnored() ? 'major' : 'blocker', 'location' => [ 'path' => $this->relativePathHelper->getRelativePath($fileSpecificError->getFile()), 'lines' => [ - 'begin' => $fileSpecificError->getLine(), + 'begin' => $fileSpecificError->getLine() ?? 0, ], ], ]; diff --git a/src/Command/ErrorFormatter/JsonErrorFormatter.php b/src/Command/ErrorFormatter/JsonErrorFormatter.php index c2ea2f6f56..4c075f0ac7 100644 --- a/src/Command/ErrorFormatter/JsonErrorFormatter.php +++ b/src/Command/ErrorFormatter/JsonErrorFormatter.php @@ -5,15 +5,14 @@ use Nette\Utils\Json; use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; +use function array_key_exists; +use function count; class JsonErrorFormatter implements ErrorFormatter { - private bool $pretty; - - public function __construct(bool $pretty) + public function __construct(private bool $pretty) { - $this->pretty = $pretty; } public function formatErrors(AnalysisResult $analysisResult, Output $output): int diff --git a/src/Command/ErrorFormatter/JunitErrorFormatter.php b/src/Command/ErrorFormatter/JunitErrorFormatter.php index 3476afed65..072f10483f 100644 --- a/src/Command/ErrorFormatter/JunitErrorFormatter.php +++ b/src/Command/ErrorFormatter/JunitErrorFormatter.php @@ -5,28 +5,28 @@ use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; use PHPStan\File\RelativePathHelper; +use function htmlspecialchars; use function sprintf; +use const ENT_COMPAT; +use const ENT_XML1; class JunitErrorFormatter implements ErrorFormatter { - private \PHPStan\File\RelativePathHelper $relativePathHelper; - - public function __construct(RelativePathHelper $relativePathHelper) + public function __construct(private RelativePathHelper $relativePathHelper) { - $this->relativePathHelper = $relativePathHelper; } public function formatErrors( AnalysisResult $analysisResult, - Output $output + Output $output, ): int { $result = ''; $result .= sprintf( '', $analysisResult->getTotalErrorsCount(), - $analysisResult->getTotalErrorsCount() + $analysisResult->getTotalErrorsCount(), ); foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { @@ -34,7 +34,7 @@ public function formatErrors( $result .= $this->createTestCase( sprintf('%s:%s', $fileName, (string) $fileSpecificError->getLine()), 'ERROR', - $this->escape($fileSpecificError->getMessage()) + $this->escape($fileSpecificError->getMessage()), ); } @@ -60,10 +60,7 @@ public function formatErrors( /** * Format a single test case * - * @param string $reference - * @param string|null $message * - * @return string */ private function createTestCase(string $reference, string $type, ?string $message = null): string { @@ -81,8 +78,6 @@ private function createTestCase(string $reference, string $type, ?string $messag /** * Escapes values for using in XML * - * @param string $string - * @return string */ private function escape(string $string): string { diff --git a/src/Command/ErrorFormatter/RawErrorFormatter.php b/src/Command/ErrorFormatter/RawErrorFormatter.php index 7d926c3b7c..4625983275 100644 --- a/src/Command/ErrorFormatter/RawErrorFormatter.php +++ b/src/Command/ErrorFormatter/RawErrorFormatter.php @@ -4,13 +4,14 @@ use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; +use function sprintf; class RawErrorFormatter implements ErrorFormatter { public function formatErrors( AnalysisResult $analysisResult, - Output $output + Output $output, ): int { foreach ($analysisResult->getNotFileSpecificErrors() as $notFileSpecificError) { @@ -24,8 +25,8 @@ public function formatErrors( '%s:%d:%s', $fileSpecificError->getFile(), $fileSpecificError->getLine() ?? '?', - $fileSpecificError->getMessage() - ) + $fileSpecificError->getMessage(), + ), ); $output->writeLineFormatted(''); } diff --git a/src/Command/ErrorFormatter/TableErrorFormatter.php b/src/Command/ErrorFormatter/TableErrorFormatter.php index 1ef40d97d1..7a7504f7cf 100644 --- a/src/Command/ErrorFormatter/TableErrorFormatter.php +++ b/src/Command/ErrorFormatter/TableErrorFormatter.php @@ -2,35 +2,33 @@ namespace PHPStan\Command\ErrorFormatter; +use PHPStan\Analyser\Error; use PHPStan\Command\AnalyseCommand; use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; use PHPStan\File\RelativePathHelper; +use Symfony\Component\Console\Formatter\OutputFormatter; +use function array_map; +use function count; +use function is_string; +use function sprintf; +use function str_replace; class TableErrorFormatter implements ErrorFormatter { - private RelativePathHelper $relativePathHelper; - - private bool $showTipsOfTheDay; - - private ?string $editorUrl; - public function __construct( - RelativePathHelper $relativePathHelper, - bool $showTipsOfTheDay, - ?string $editorUrl = null + private RelativePathHelper $relativePathHelper, + private bool $showTipsOfTheDay, + private ?string $editorUrl, ) { - $this->relativePathHelper = $relativePathHelper; - $this->showTipsOfTheDay = $showTipsOfTheDay; - $this->editorUrl = $editorUrl; } /** @api */ public function formatErrors( AnalysisResult $analysisResult, - Output $output + Output $output, ): int { $projectConfigFile = 'phpstan.neon'; @@ -48,7 +46,7 @@ public function formatErrors( $output->writeLineFormatted(sprintf( "PHPStan is performing only the most basic checks.\nYou can pass a higher rule level through the --%s option\n(the default and current level is %d) to analyse code more thoroughly.", AnalyseCommand::OPTION_LEVEL, - AnalyseCommand::DEFAULT_LEVEL + AnalyseCommand::DEFAULT_LEVEL, )); $output->writeLineFormatted(''); } @@ -57,7 +55,7 @@ public function formatErrors( return 0; } - /** @var array $fileErrors */ + /** @var array $fileErrors */ $fileErrors = []; foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { if (!isset($fileErrors[$fileSpecificError->getFile()])) { @@ -77,7 +75,9 @@ public function formatErrors( $message .= "\n💡 " . $tip; } if (is_string($this->editorUrl)) { - $message .= "\n✏️ " . str_replace(['%file%', '%line%'], [$error->getTraitFilePath() ?? $error->getFilePath(), (string) $error->getLine()], $this->editorUrl); + $editorFile = $error->getTraitFilePath() ?? $error->getFilePath(); + $url = str_replace(['%file%', '%line%'], [$editorFile, (string) $error->getLine()], $this->editorUrl); + $message .= "\n✏️ ' . $this->relativePathHelper->getRelativePath($editorFile) . ''; } $rows[] = [ (string) $error->getLine(), @@ -85,22 +85,16 @@ public function formatErrors( ]; } - $relativeFilePath = $this->relativePathHelper->getRelativePath($file); - - $style->table(['Line', $relativeFilePath], $rows); + $style->table(['Line', $this->relativePathHelper->getRelativePath($file)], $rows); } if (count($analysisResult->getNotFileSpecificErrors()) > 0) { - $style->table(['', 'Error'], array_map(static function (string $error): array { - return ['', $error]; - }, $analysisResult->getNotFileSpecificErrors())); + $style->table(['', 'Error'], array_map(static fn (string $error): array => ['', $error], $analysisResult->getNotFileSpecificErrors())); } $warningsCount = count($analysisResult->getWarnings()); if ($warningsCount > 0) { - $style->table(['', 'Warning'], array_map(static function (string $warning): array { - return ['', $warning]; - }, $analysisResult->getWarnings())); + $style->table(['', 'Warning'], array_map(static fn (string $warning): array => ['', $warning], $analysisResult->getWarnings())); } $finalMessage = sprintf($analysisResult->getTotalErrorsCount() === 1 ? 'Found %d error' : 'Found %d errors', $analysisResult->getTotalErrorsCount()); diff --git a/src/Command/ErrorFormatter/TeamcityErrorFormatter.php b/src/Command/ErrorFormatter/TeamcityErrorFormatter.php index 17bbb080af..a6e6e2cf32 100644 --- a/src/Command/ErrorFormatter/TeamcityErrorFormatter.php +++ b/src/Command/ErrorFormatter/TeamcityErrorFormatter.php @@ -5,6 +5,12 @@ use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; use PHPStan\File\RelativePathHelper; +use function array_keys; +use function array_values; +use function count; +use function is_string; +use function preg_replace; +use const PHP_EOL; /** * @see https://www.jetbrains.com/help/teamcity/build-script-interaction-with-teamcity.html#Reporting+Inspections @@ -12,11 +18,8 @@ class TeamcityErrorFormatter implements ErrorFormatter { - private RelativePathHelper $relativePathHelper; - - public function __construct(RelativePathHelper $relativePathHelper) + public function __construct(private RelativePathHelper $relativePathHelper) { - $this->relativePathHelper = $relativePathHelper; } public function formatErrors(AnalysisResult $analysisResult, Output $output): int diff --git a/src/Command/ErrorsConsoleStyle.php b/src/Command/ErrorsConsoleStyle.php index f106573a69..76954e735f 100644 --- a/src/Command/ErrorsConsoleStyle.php +++ b/src/Command/ErrorsConsoleStyle.php @@ -4,17 +4,28 @@ use OndraM\CiDetector\CiDetector; use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; - -class ErrorsConsoleStyle extends \Symfony\Component\Console\Style\SymfonyStyle +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Console\Terminal; +use function array_unshift; +use function explode; +use function implode; +use function sprintf; +use function str_starts_with; +use function strlen; +use function wordwrap; +use const DIRECTORY_SEPARATOR; + +class ErrorsConsoleStyle extends SymfonyStyle { public const OPTION_NO_PROGRESS = 'no-progress'; private bool $showProgress; - private \Symfony\Component\Console\Helper\ProgressBar $progressBar; + private ProgressBar $progressBar; private ?bool $isCiDetected = null; @@ -41,7 +52,7 @@ private function isCiDetected(): bool public function table(array $headers, array $rows): void { /** @var int $terminalWidth */ - $terminalWidth = (new \Symfony\Component\Console\Terminal())->getWidth() - 2; + $terminalWidth = (new Terminal())->getWidth() - 2; $maxHeaderWidth = strlen($headers[0]); foreach ($rows as $row) { $length = strlen($row[0]); @@ -52,42 +63,80 @@ public function table(array $headers, array $rows): void $maxHeaderWidth = $length; } - $wrap = static function ($rows) use ($terminalWidth, $maxHeaderWidth): array { - return array_map(static function ($row) use ($terminalWidth, $maxHeaderWidth): array { - return array_map(static function ($s) use ($terminalWidth, $maxHeaderWidth) { - if ($terminalWidth > $maxHeaderWidth + 5) { - return wordwrap( - $s, - $terminalWidth - $maxHeaderWidth - 5, - "\n", - true - ); - } - - return $s; - }, $row); - }, $rows); - }; - - parent::table($headers, $wrap($rows)); + // manual wrapping could be replaced with $table->setColumnMaxWidth() + // but it's buggy for lines + // https://github.com/symfony/symfony/issues/45520 + // https://github.com/symfony/symfony/issues/45521 + $headers = $this->wrap($headers, $terminalWidth, $maxHeaderWidth); + foreach ($headers as $i => $header) { + $newHeader = []; + foreach (explode("\n", $header) as $h) { + $newHeader[] = sprintf('%s', $h); + } + + $headers[$i] = implode("\n", $newHeader); + } + + foreach ($rows as $i => $row) { + $rows[$i] = $this->wrap($row, $terminalWidth, $maxHeaderWidth); + } + + $table = $this->createTable(); + array_unshift($rows, $headers, new TableSeparator()); + $table->setRows($rows); + + $table->render(); + $this->newLine(); } /** - * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint - * @param int $max + * @param string[] $rows + * @return string[] */ - public function createProgressBar($max = 0): ProgressBar + private function wrap(array $rows, int $terminalWidth, int $maxHeaderWidth): array + { + foreach ($rows as $i => $column) { + $columnRows = explode("\n", $column); + foreach ($columnRows as $k => $columnRow) { + if (str_starts_with($columnRow, '✏️')) { + continue; + } + $columnRows[$k] = wordwrap( + $columnRow, + $terminalWidth - $maxHeaderWidth - 5, + "\n", + true, + ); + } + + $rows[$i] = implode("\n", $columnRows); + } + + return $rows; + } + + public function createProgressBar(int $max = 0): ProgressBar { $this->progressBar = parent::createProgressBar($max); - $this->progressBar->setOverwrite(!$this->isCiDetected()); + + $ci = $this->isCiDetected(); + $this->progressBar->setOverwrite(!$ci); + + if ($ci) { + $this->progressBar->minSecondsBetweenRedraws(15); + $this->progressBar->maxSecondsBetweenRedraws(30); + } elseif (DIRECTORY_SEPARATOR === '\\') { + $this->progressBar->minSecondsBetweenRedraws(0.5); + $this->progressBar->maxSecondsBetweenRedraws(2); + } else { + $this->progressBar->minSecondsBetweenRedraws(0.1); + $this->progressBar->maxSecondsBetweenRedraws(0.5); + } + return $this->progressBar; } - /** - * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint - * @param int $max - */ - public function progressStart($max = 0): void + public function progressStart(int $max = 0): void { if (!$this->showProgress) { return; @@ -95,25 +144,12 @@ public function progressStart($max = 0): void parent::progressStart($max); } - /** - * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint - * @param int $step - */ - public function progressAdvance($step = 1): void + public function progressAdvance(int $step = 1): void { if (!$this->showProgress) { return; } - if (!$this->isCiDetected() && $step > 0) { - $stepTime = (time() - $this->progressBar->getStartTime()) / $step; - if ($stepTime > 0 && $stepTime < 1) { - $this->progressBar->setRedrawFrequency((int) (1 / $stepTime)); - } else { - $this->progressBar->setRedrawFrequency(1); - } - } - parent::progressAdvance($step); } diff --git a/src/Command/FixerApplication.php b/src/Command/FixerApplication.php index 957dc82bb0..9d01812a39 100644 --- a/src/Command/FixerApplication.php +++ b/src/Command/FixerApplication.php @@ -5,22 +5,32 @@ use Clue\React\NDJson\Decoder; use Clue\React\NDJson\Encoder; use Composer\CaBundle\CaBundle; +use DateTime; +use DateTimeImmutable; +use DateTimeZone; +use Jean85\PrettyVersions; use Nette\Utils\Json; +use OutOfBoundsException; use Phar; use PHPStan\Analyser\AnalyserResult; +use PHPStan\Analyser\Error; use PHPStan\Analyser\IgnoredErrorHelper; use PHPStan\Analyser\ResultCache\ResultCacheClearer; use PHPStan\Analyser\ResultCache\ResultCacheManagerFactory; +use PHPStan\File\CouldNotReadFileException; use PHPStan\File\FileMonitor; use PHPStan\File\FileMonitorResult; use PHPStan\File\FileReader; use PHPStan\File\FileWriter; use PHPStan\Parallel\Scheduler; use PHPStan\Process\CpuCoreCounter; +use PHPStan\Process\ProcessCanceledException; +use PHPStan\Process\ProcessCrashedException; 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\EventLoop\LoopInterface; @@ -31,88 +41,65 @@ use React\Promise\PromiseInterface; use React\Socket\ConnectionInterface; use React\Socket\Connector; +use React\Socket\TcpServer; +use React\Stream\ReadableStreamInterface; +use RuntimeException; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use const PHP_BINARY; +use Throwable; use function Clue\React\Block\await; +use function count; +use function defined; use function escapeshellarg; -use function file_exists; +use function fclose; +use function fopen; +use function fwrite; +use function getenv; +use function ini_get; +use function is_dir; +use function is_file; +use function is_string; +use function min; +use function mkdir; +use function parse_url; use function React\Promise\resolve; +use function sprintf; +use function strlen; +use function strpos; +use function unlink; +use const PHP_BINARY; +use const PHP_URL_PORT; class FixerApplication { - /** @var FileMonitor */ - private $fileMonitor; - - /** @var ResultCacheManagerFactory */ - private $resultCacheManagerFactory; - - /** @var ResultCacheClearer */ - private $resultCacheClearer; - - /** @var IgnoredErrorHelper */ - private $ignoredErrorHelper; - - /** @var CpuCoreCounter */ - private $cpuCoreCounter; - - /** @var Scheduler */ - private $scheduler; - - /** @var string[] */ - private $analysedPaths; - /** @var (ExtendedPromiseInterface&CancellablePromiseInterface)|null */ private $processInProgress; - /** @var string */ - private $currentWorkingDirectory; - - /** @var string */ - private $fixerTmpDir; - - private int $maximumNumberOfProcesses; - - /** @var string|null */ - private $fixerSuggestionId; + private ?string $fixerSuggestionId = null; /** - * @param FileMonitor $fileMonitor - * @param ResultCacheManagerFactory $resultCacheManagerFactory * @param string[] $analysedPaths */ public function __construct( - FileMonitor $fileMonitor, - ResultCacheManagerFactory $resultCacheManagerFactory, - ResultCacheClearer $resultCacheClearer, - IgnoredErrorHelper $ignoredErrorHelper, - CpuCoreCounter $cpuCoreCounter, - Scheduler $scheduler, - array $analysedPaths, - string $currentWorkingDirectory, - string $fixerTmpDir, - int $maximumNumberOfProcesses + 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, ) { - $this->fileMonitor = $fileMonitor; - $this->resultCacheManagerFactory = $resultCacheManagerFactory; - $this->resultCacheClearer = $resultCacheClearer; - $this->ignoredErrorHelper = $ignoredErrorHelper; - $this->cpuCoreCounter = $cpuCoreCounter; - $this->scheduler = $scheduler; - $this->analysedPaths = $analysedPaths; - $this->currentWorkingDirectory = $currentWorkingDirectory; - $this->fixerTmpDir = $fixerTmpDir; - $this->maximumNumberOfProcesses = $maximumNumberOfProcesses; } /** - * @param \Symfony\Component\Console\Output\OutputInterface $output - * @param \PHPStan\Analyser\Error[] $fileSpecificErrors + * @param Error[] $fileSpecificErrors * @param string[] $notFileSpecificErrors - * @return int */ public function run( ?string $projectConfigFile, @@ -122,11 +109,11 @@ public function run( array $fileSpecificErrors, array $notFileSpecificErrors, int $filesCount, - string $mainScript + string $mainScript, ): int { $loop = new StreamSelectLoop(); - $server = new \React\Socket\TcpServer('127.0.0.1:0', $loop); + $server = new TcpServer('127.0.0.1:0', $loop); /** @var string $serverAddress */ $serverAddress = $server->getAddress(); @@ -141,12 +128,15 @@ public function log(string $message): void } }, - min($this->cpuCoreCounter->getNumberOfCpuCores(), $this->maximumNumberOfProcesses) + min($this->cpuCoreCounter->getNumberOfCpuCores(), $this->maximumNumberOfProcesses), ); $server->on('connection', function (ConnectionInterface $connection) use ($loop, $projectConfigFile, $input, $output, $fileSpecificErrors, $notFileSpecificErrors, $mainScript, $filesCount, $reanalyseProcessQueue, $inceptionResult): void { - $decoder = new Decoder($connection, true, 512, defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0, 128 * 1024 * 1024); - $encoder = new Encoder($connection, defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0); + // phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly + $jsonInvalidUtf8Ignore = defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0; + // phpcs:enable + $decoder = new Decoder($connection, true, 512, $jsonInvalidUtf8Ignore, 128 * 1024 * 1024); + $encoder = new Encoder($connection, $jsonInvalidUtf8Ignore); $encoder->write(['action' => 'initialData', 'data' => [ 'fileSpecificErrors' => $fileSpecificErrors, 'notFileSpecificErrors' => $notFileSpecificErrors, @@ -189,16 +179,16 @@ public function log(string $message): void $data['data']['tmpFile'], $data['data']['insteadOfFile'], $data['data']['fixerSuggestionId'], - $input + $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 \PHPStan\Process\ProcessCrashedException) { + }, 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 \PHPStan\Process\ProcessCanceledException) { + if ($e instanceof ProcessCanceledException) { $encoder->write(['id' => $id, 'error' => $e->getMessage()]); return; } @@ -224,7 +214,7 @@ public function log(string $message): void $mainScript, $projectConfigFile, $this->fixerSuggestionId, - $input + $input, )->done(function (array $json) use ($encoder, $changes): void { $this->processInProgress = null; $this->fixerSuggestionId = null; @@ -234,7 +224,7 @@ public function log(string $message): void 'filesCount' => $changes->getTotalFilesCount(), ]]); $this->resultCacheClearer->clearTemporaryCaches(); - }, function (\Throwable $e) use ($encoder, $output): void { + }, function (Throwable $e) use ($encoder, $output): void { $this->processInProgress = null; $this->fixerSuggestionId = null; $output->writeln('Worker process exited: ' . $e->getMessage() . ''); @@ -247,7 +237,7 @@ public function log(string $message): void try { $fixerProcess = $this->getFixerProcess($output, $serverPort); - } catch (\PHPStan\Command\FixerProcessException $e) { + } catch (FixerProcessException) { return 1; } @@ -275,7 +265,7 @@ private function getFixerProcess(OutputInterface $output, int $serverPort): Proc { if (!@mkdir($this->fixerTmpDir, 0777) && !is_dir($this->fixerTmpDir)) { $output->writeln(sprintf('Cannot create a temp directory %s', $this->fixerTmpDir)); - throw new \PHPStan\Command\FixerProcessException(); + throw new FixerProcessException(); } $pharPath = $this->fixerTmpDir . '/phpstan-fixer.phar'; @@ -283,12 +273,12 @@ private function getFixerProcess(OutputInterface $output, int $serverPort): Proc try { $this->downloadPhar($output, $pharPath, $infoPath); - } catch (\RuntimeException $e) { - if (!file_exists($pharPath)) { + } catch (RuntimeException $e) { + if (!is_file($pharPath)) { $output->writeln('Could not download the PHPStan Pro executable.'); $output->writeln($e->getMessage()); - throw new \PHPStan\Command\FixerProcessException(); + throw new FixerProcessException(); } } @@ -297,12 +287,12 @@ private function getFixerProcess(OutputInterface $output, int $serverPort): Proc try { $phar = new Phar($pharPath); - } catch (\Throwable $e) { + } catch (Throwable) { @unlink($pharPath); @unlink($infoPath); $output->writeln('PHPStan Pro PHAR signature is corrupted.'); - throw new \PHPStan\Command\FixerProcessException(); + throw new FixerProcessException(); } if ($phar->getSignature()['hash_type'] !== 'OpenSSL') { @@ -310,10 +300,10 @@ private function getFixerProcess(OutputInterface $output, int $serverPort): Proc @unlink($infoPath); $output->writeln('PHPStan Pro PHAR signature is corrupted.'); - throw new \PHPStan\Command\FixerProcessException(); + throw new FixerProcessException(); } - $env = null; + $env = getenv(); $forcedPort = $_SERVER['PHPSTAN_PRO_WEB_PORT'] ?? null; if ($forcedPort !== null) { $env['PHPSTAN_PRO_WEB_PORT'] = $_SERVER['PHPSTAN_PRO_WEB_PORT']; @@ -352,19 +342,19 @@ private function getFixerProcess(OutputInterface $output, int $serverPort): Proc private function downloadPhar( OutputInterface $output, string $pharPath, - string $infoPath + string $infoPath, ): void { $currentVersion = null; - if (file_exists($pharPath) && file_exists($infoPath)) { + if (is_file($pharPath) && is_file($infoPath)) { /** @var array{version: string, date: string} $currentInfo */ $currentInfo = Json::decode(FileReader::read($infoPath), Json::FORCE_ARRAY); $currentVersion = $currentInfo['version']; - $currentDate = \DateTime::createFromFormat(\DateTime::ATOM, $currentInfo['date']); + $currentDate = DateTime::createFromFormat(DateTime::ATOM, $currentInfo['date']); if ($currentDate === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - if ((new \DateTimeImmutable('', new \DateTimeZone('UTC'))) <= $currentDate->modify('+24 hours')) { + if ((new DateTimeImmutable('', new DateTimeZone('UTC'))) <= $currentDate->modify('+24 hours')) { return; } @@ -382,12 +372,15 @@ private function downloadPhar( 'cafile' => CaBundle::getBundledCaBundlePath(), ], 'dns' => '1.1.1.1', - ] - ) + ], + ), ); - /** @var array{url: string, version: string} $latestInfo */ - $latestInfo = Json::decode((string) await($client->get('https://fixer-download-api.phpstan.com/latest'), $loop, 5.0)->getBody(), Json::FORCE_ARRAY); // @phpstan-ignore-line + /** + * @var array{url: string, version: string} $latestInfo + * @phpstan-ignore-next-line + */ + $latestInfo = Json::decode((string) await($client->get('https://fixer-download-api.phpstan.com/latest'), $loop, 5.0)->getBody(), Json::FORCE_ARRAY); if ($currentVersion !== null && $latestInfo['version'] === $currentVersion) { $this->writeInfoFile($infoPath, $latestInfo['version']); $output->writeln('You\'re running the latest PHPStan Pro!'); @@ -398,13 +391,13 @@ private function downloadPhar( $pharPathResource = fopen($pharPath, 'w'); if ($pharPathResource === false) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Could not open file %s for writing.', $pharPath)); + throw new ShouldNotHappenException(sprintf('Could not open file %s for writing.', $pharPath)); } $progressBar = new ProgressBar($output); $client->requestStreaming('GET', $latestInfo['url'])->done(static function (ResponseInterface $response) use ($progressBar, $pharPathResource): void { $body = $response->getBody(); - if (!$body instanceof \React\Stream\ReadableStreamInterface) { - throw new \PHPStan\ShouldNotHappenException(); + if (!$body instanceof ReadableStreamInterface) { + throw new ShouldNotHappenException(); } $totalSize = (int) $response->getHeaderLine('Content-Length'); @@ -418,7 +411,7 @@ private function downloadPhar( fwrite($pharPathResource, $chunk); $progressBar->setProgress($bytes); }); - }, static function (\Throwable $e) use ($output): void { + }, static function (Throwable $e) use ($output): void { $output->writeln(sprintf('Could not download the PHPStan Pro executable: %s', $e->getMessage())); }); @@ -437,12 +430,11 @@ private function writeInfoFile(string $infoPath, string $version): void { FileWriter::write($infoPath, Json::encode([ 'version' => $version, - 'date' => (new \DateTimeImmutable('', new \DateTimeZone('UTC')))->format(\DateTime::ATOM), + 'date' => (new DateTimeImmutable('', new DateTimeZone('UTC')))->format(DateTime::ATOM), ])); } /** - * @param LoopInterface $loop * @param callable(FileMonitorResult): void $hasChangesCallback */ private function monitorFileChanges(LoopInterface $loop, callable $hasChangesCallback): void @@ -468,7 +460,7 @@ private function reanalyseWithTmpFile( string $tmpFile, string $insteadOfFile, string $fixerSuggestionId, - InputInterface $input + InputInterface $input, ): PromiseInterface { $resultCacheManager = $this->resultCacheManagerFactory->create([$insteadOfFile => $tmpFile]); @@ -489,7 +481,7 @@ private function reanalyseWithTmpFile( escapeshellarg($fixerSuggestionId), '--allow-parallel', ], - $input + $input, )); return $runnableQueue->queue($process, $schedule->getNumberOfProcesses()); @@ -501,12 +493,12 @@ private function reanalyseAfterFileChanges( string $mainScript, ?string $projectConfigFile, ?string $fixerSuggestionId, - InputInterface $input + InputInterface $input, ): PromiseInterface { $ignoredErrorHelperResult = $this->ignoredErrorHelper->initialize(); if (count($ignoredErrorHelperResult->getErrors()) > 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $projectConfigArray = $inceptionResult->getProjectConfigArray(); @@ -520,13 +512,13 @@ private function reanalyseAfterFileChanges( $resultCache, $inceptionResult->getErrorOutput(), false, - true + true, )->getAnalyserResult(); $intermediateErrors = $ignoredErrorHelperResult->process( $result->getErrors(), $isOnlyFiles, $inceptionFiles, - count($result->getInternalErrors()) > 0 || $result->hasReachedInternalErrorsCountLimit() + count($result->getInternalErrors()) > 0 || $result->hasReachedInternalErrorsCountLimit(), ); $finalFileSpecificErrors = []; $finalNotFileSpecificErrors = []; @@ -555,20 +547,18 @@ private function reanalyseAfterFileChanges( 'fixer:worker', $projectConfigFile, $options, - $input + $input, )); $this->processInProgress = $process->run(); - return $this->processInProgress->then(static function (string $output): array { - return Json::decode($output, Json::FORCE_ARRAY); - }); + return $this->processInProgress->then(static fn (string $output): array => Json::decode($output, Json::FORCE_ARRAY)); } private function getPhpstanVersion(): string { try { - return \Jean85\PrettyVersions::getVersion('phpstan/phpstan')->getPrettyVersion(); - } catch (\OutOfBoundsException $e) { + return PrettyVersions::getVersion('phpstan/phpstan')->getPrettyVersion(); + } catch (OutOfBoundsException) { return 'Version unknown'; } } @@ -583,7 +573,7 @@ private function isDockerRunning(): bool $contents = FileReader::read('/proc/1/cgroup'); return strpos($contents, 'docker') !== false; - } catch (\PHPStan\File\CouldNotReadFileException $e) { + } catch (CouldNotReadFileException) { return false; } } diff --git a/src/Command/FixerProcessException.php b/src/Command/FixerProcessException.php index 996000c6ad..9473f35716 100644 --- a/src/Command/FixerProcessException.php +++ b/src/Command/FixerProcessException.php @@ -2,7 +2,9 @@ namespace PHPStan\Command; -class FixerProcessException extends \Exception +use Exception; + +class FixerProcessException extends Exception { } diff --git a/src/Command/FixerWorkerCommand.php b/src/Command/FixerWorkerCommand.php index 3a82fac66f..72ec027415 100644 --- a/src/Command/FixerWorkerCommand.php +++ b/src/Command/FixerWorkerCommand.php @@ -7,29 +7,30 @@ use PHPStan\Analyser\IgnoredErrorHelper; use PHPStan\Analyser\ResultCache\ResultCacheManager; use PHPStan\Analyser\ResultCache\ResultCacheManagerFactory; +use PHPStan\ShouldNotHappenException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use function count; +use function is_array; +use function is_bool; +use function is_string; class FixerWorkerCommand extends Command { private const NAME = 'fixer:worker'; - /** @var string[] */ - private $composerAutoloaderProjectPaths; - /** * @param string[] $composerAutoloaderProjectPaths */ public function __construct( - array $composerAutoloaderProjectPaths + private array $composerAutoloaderProjectPaths, ) { parent::__construct(); - $this->composerAutoloaderProjectPaths = $composerAutoloaderProjectPaths; } protected function configure(): void @@ -38,7 +39,6 @@ protected function configure(): void ->setDescription('(Internal) Support for PHPStan Pro.') ->setDefinition([ new InputArgument('paths', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Paths with source code to run analysis on'), - new InputOption('paths-file', null, InputOption::VALUE_REQUIRED, 'Path to a file with a list of paths to run analysis on'), new InputOption('configuration', 'c', InputOption::VALUE_REQUIRED, 'Path to project configuration file'), new InputOption(AnalyseCommand::OPTION_LEVEL, 'l', InputOption::VALUE_REQUIRED, 'Level of rule options - the higher the stricter'), new InputOption('autoload-file', 'a', InputOption::VALUE_REQUIRED, 'Project\'s additional autoload file path'), @@ -59,7 +59,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $autoloadFile = $input->getOption('autoload-file'); $configuration = $input->getOption('configuration'); $level = $input->getOption(AnalyseCommand::OPTION_LEVEL); - $pathsFile = $input->getOption('paths-file'); $allowXdebug = $input->getOption('xdebug'); $allowParallel = $input->getOption('allow-parallel'); @@ -69,11 +68,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int || (!is_string($autoloadFile) && $autoloadFile !== null) || (!is_string($configuration) && $configuration !== null) || (!is_string($level) && $level !== null) - || (!is_string($pathsFile) && $pathsFile !== null) || (!is_bool($allowXdebug)) || (!is_bool($allowParallel)) ) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } /** @var string|null $tmpFile */ @@ -89,12 +87,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int $restoreResultCache = $input->getOption('restore-result-cache'); if (is_string($tmpFile)) { if (!is_string($insteadOfFile)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } } elseif (is_string($insteadOfFile)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } elseif ($saveResultCache === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $singleReflectionFile = null; @@ -107,7 +105,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $input, $output, $paths, - $pathsFile, $memoryLimit, $autoloadFile, $this->composerAutoloaderProjectPaths, @@ -116,11 +113,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $level, $allowXdebug, false, - false, $singleReflectionFile, - $insteadOfFile + $insteadOfFile, + false, ); - } catch (\PHPStan\Command\InceptionNotSuccessfulException $e) { + } catch (InceptionNotSuccessfulException) { return 1; } @@ -130,7 +127,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $ignoredErrorHelper = $container->getByType(IgnoredErrorHelper::class); $ignoredErrorHelperResult = $ignoredErrorHelper->initialize(); if (count($ignoredErrorHelperResult->getErrors()) > 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } /** @var AnalyserRunner $analyserRunner */ @@ -156,21 +153,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int $configuration, $tmpFile, $insteadOfFile, - $input + $input, ); $result = $resultCacheManager->process( $this->switchTmpFileInAnalyserResult($intermediateAnalyserResult, $tmpFile, $insteadOfFile), $resultCache, $inceptionResult->getErrorOutput(), false, - is_string($saveResultCache) ? $saveResultCache : $saveResultCache === null + is_string($saveResultCache) ? $saveResultCache : $saveResultCache === null, )->getAnalyserResult(); $intermediateErrors = $ignoredErrorHelperResult->process( $result->getErrors(), $isOnlyFiles, $inceptionFiles, - count($result->getInternalErrors()) > 0 || $result->hasReachedInternalErrorsCountLimit() + count($result->getInternalErrors()) > 0 || $result->hasReachedInternalErrorsCountLimit(), ); $finalFileSpecificErrors = []; $finalNotFileSpecificErrors = []; @@ -194,7 +191,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int private function switchTmpFileInAnalyserResult( AnalyserResult $analyserResult, ?string $insteadOfFile, - ?string $tmpFile + ?string $tmpFile, ): AnalyserResult { $fileSpecificErrors = []; @@ -255,7 +252,7 @@ private function switchTmpFileInAnalyserResult( $analyserResult->getInternalErrors(), $dependencies, $exportedNodes, - $analyserResult->hasReachedInternalErrorsCountLimit() + $analyserResult->hasReachedInternalErrorsCountLimit(), ); } diff --git a/src/Command/IgnoredRegexValidator.php b/src/Command/IgnoredRegexValidator.php index 7e648f86e8..59efbf03b5 100644 --- a/src/Command/IgnoredRegexValidator.php +++ b/src/Command/IgnoredRegexValidator.php @@ -4,26 +4,26 @@ use Hoa\Compiler\Llk\Parser; use Hoa\Compiler\Llk\TreeNode; +use Hoa\Exception\Exception; use Nette\Utils\Strings; use PHPStan\PhpDoc\TypeStringResolver; +use PHPStan\PhpDocParser\Parser\ParserException; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\ObjectType; use PHPStan\Type\VerbosityLevel; +use function count; +use function strpos; +use function strrpos; use function substr; class IgnoredRegexValidator { - private Parser $parser; - - private \PHPStan\PhpDoc\TypeStringResolver $typeStringResolver; - public function __construct( - Parser $parser, - TypeStringResolver $typeStringResolver + private Parser $parser, + private TypeStringResolver $typeStringResolver, ) { - $this->parser = $parser; - $this->typeStringResolver = $typeStringResolver; } public function validate(string $regex): IgnoredRegexValidatorResult @@ -33,7 +33,7 @@ public function validate(string $regex): IgnoredRegexValidatorResult try { /** @var TreeNode $ast */ $ast = $this->parser->parse($regex); - } catch (\Hoa\Exception\Exception $e) { + } catch (Exception $e) { if (strpos($e->getMessage(), 'Unexpected token "|" (alternation) at line 1') === 0) { return new IgnoredRegexValidatorResult([], false, true, '||', '\|\|'); } @@ -49,12 +49,11 @@ public function validate(string $regex): IgnoredRegexValidatorResult return new IgnoredRegexValidatorResult( $this->getIgnoredTypes($ast), $this->hasAnchorsInTheMiddle($ast), - false + false, ); } /** - * @param TreeNode $ast * @return array */ private function getIgnoredTypes(TreeNode $ast): array @@ -83,7 +82,7 @@ private function getIgnoredTypes(TreeNode $ast): array try { $type = $this->typeStringResolver->resolve($matches[1], null); - } catch (\PHPStan\PhpDocParser\Parser\ParserException $e) { + } catch (ParserException) { continue; } @@ -106,7 +105,7 @@ private function removeDelimiters(string $regex): string $delimiter = substr($regex, 0, 1); $endDelimiterPosition = strrpos($regex, $delimiter); if ($endDelimiterPosition === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return substr($regex, 1, $endDelimiterPosition - 1); diff --git a/src/Command/IgnoredRegexValidatorResult.php b/src/Command/IgnoredRegexValidatorResult.php index 0acda5dd11..fcda6a015b 100644 --- a/src/Command/IgnoredRegexValidatorResult.php +++ b/src/Command/IgnoredRegexValidatorResult.php @@ -5,35 +5,17 @@ class IgnoredRegexValidatorResult { - /** @var array */ - private array $ignoredTypes; - - private bool $anchorsInTheMiddle; - - private bool $allErrorsIgnored; - - private ?string $wrongSequence; - - private ?string $escapedWrongSequence; - /** * @param array $ignoredTypes - * @param bool $anchorsInTheMiddle - * @param bool $allErrorsIgnored */ public function __construct( - array $ignoredTypes, - bool $anchorsInTheMiddle, - bool $allErrorsIgnored, - ?string $wrongSequence = null, - ?string $escapedWrongSequence = null + private array $ignoredTypes, + private bool $anchorsInTheMiddle, + private bool $allErrorsIgnored, + private ?string $wrongSequence = null, + private ?string $escapedWrongSequence = null, ) { - $this->ignoredTypes = $ignoredTypes; - $this->anchorsInTheMiddle = $anchorsInTheMiddle; - $this->allErrorsIgnored = $allErrorsIgnored; - $this->wrongSequence = $wrongSequence; - $this->escapedWrongSequence = $escapedWrongSequence; } /** diff --git a/src/Command/InceptionNotSuccessfulException.php b/src/Command/InceptionNotSuccessfulException.php index 872b71cfaa..f17c3e08ec 100644 --- a/src/Command/InceptionNotSuccessfulException.php +++ b/src/Command/InceptionNotSuccessfulException.php @@ -2,7 +2,9 @@ namespace PHPStan\Command; -class InceptionNotSuccessfulException extends \Exception +use Exception; + +class InceptionNotSuccessfulException extends Exception { } diff --git a/src/Command/InceptionResult.php b/src/Command/InceptionResult.php index d0b778a9d8..8a0010c8c2 100644 --- a/src/Command/InceptionResult.php +++ b/src/Command/InceptionResult.php @@ -5,6 +5,7 @@ use PHPStan\DependencyInjection\Container; use PHPStan\Internal\BytesHelper; use function memory_get_peak_usage; +use function sprintf; class InceptionResult { @@ -12,55 +13,22 @@ class InceptionResult /** @var callable(): (array{string[], bool}) */ private $filesCallback; - private Output $stdOutput; - - private Output $errorOutput; - - private \PHPStan\DependencyInjection\Container $container; - - private bool $isDefaultLevelUsed; - - private string $memoryLimitFile; - - private ?string $projectConfigFile; - - /** @var mixed[]|null */ - private ?array $projectConfigArray; - - private ?string $generateBaselineFile; - /** * @param callable(): (array{string[], bool}) $filesCallback - * @param Output $stdOutput - * @param Output $errorOutput - * @param \PHPStan\DependencyInjection\Container $container - * @param bool $isDefaultLevelUsed - * @param string $memoryLimitFile - * @param string|null $projectConfigFile - * @param mixed[] $projectConfigArray - * @param string|null $generateBaselineFile + * @param mixed[]|null $projectConfigArray */ public function __construct( callable $filesCallback, - Output $stdOutput, - Output $errorOutput, - Container $container, - bool $isDefaultLevelUsed, - string $memoryLimitFile, - ?string $projectConfigFile, - ?array $projectConfigArray, - ?string $generateBaselineFile + private Output $stdOutput, + private Output $errorOutput, + private Container $container, + private bool $isDefaultLevelUsed, + private ?string $projectConfigFile, + private ?array $projectConfigArray, + private ?string $generateBaselineFile, ) { $this->filesCallback = $filesCallback; - $this->stdOutput = $stdOutput; - $this->errorOutput = $errorOutput; - $this->container = $container; - $this->isDefaultLevelUsed = $isDefaultLevelUsed; - $this->memoryLimitFile = $memoryLimitFile; - $this->projectConfigFile = $projectConfigFile; - $this->projectConfigArray = $projectConfigArray; - $this->generateBaselineFile = $generateBaselineFile; } /** @@ -117,7 +85,6 @@ public function handleReturn(int $exitCode): int $this->getErrorOutput()->writeLineFormatted(sprintf('Used memory: %s', BytesHelper::bytes(memory_get_peak_usage(true)))); } - @unlink($this->memoryLimitFile); return $exitCode; } diff --git a/src/Command/Symfony/SymfonyOutput.php b/src/Command/Symfony/SymfonyOutput.php index ba12d8206d..5e1a46273a 100644 --- a/src/Command/Symfony/SymfonyOutput.php +++ b/src/Command/Symfony/SymfonyOutput.php @@ -12,17 +12,11 @@ class SymfonyOutput implements Output { - private \Symfony\Component\Console\Output\OutputInterface $symfonyOutput; - - private OutputStyle $style; - public function __construct( - OutputInterface $symfonyOutput, - OutputStyle $style + private OutputInterface $symfonyOutput, + private OutputStyle $style, ) { - $this->symfonyOutput = $symfonyOutput; - $this->style = $style; } public function writeFormatted(string $message): void diff --git a/src/Command/Symfony/SymfonyStyle.php b/src/Command/Symfony/SymfonyStyle.php index ba2f25ee16..99ed87ec33 100644 --- a/src/Command/Symfony/SymfonyStyle.php +++ b/src/Command/Symfony/SymfonyStyle.php @@ -11,14 +11,11 @@ class SymfonyStyle implements OutputStyle { - private \Symfony\Component\Console\Style\StyleInterface $symfonyStyle; - - public function __construct(StyleInterface $symfonyStyle) + public function __construct(private StyleInterface $symfonyStyle) { - $this->symfonyStyle = $symfonyStyle; } - public function getSymfonyStyle(): \Symfony\Component\Console\Style\StyleInterface + public function getSymfonyStyle(): StyleInterface { return $this->symfonyStyle; } diff --git a/src/Command/WorkerCommand.php b/src/Command/WorkerCommand.php index e1154d3829..c55d30bd62 100644 --- a/src/Command/WorkerCommand.php +++ b/src/Command/WorkerCommand.php @@ -7,7 +7,9 @@ use PHPStan\Analyser\FileAnalyser; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\DependencyInjection\Container; +use PHPStan\File\PathNotFoundException; use PHPStan\Rules\Registry; +use PHPStan\ShouldNotHappenException; use React\EventLoop\StreamSelectLoop; use React\Socket\ConnectionInterface; use React\Socket\TcpConnector; @@ -18,26 +20,32 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; 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; +use function is_bool; +use function is_string; +use function sprintf; class WorkerCommand extends Command { private const NAME = 'worker'; - /** @var string[] */ - private array $composerAutoloaderProjectPaths; - private int $errorCount = 0; /** * @param string[] $composerAutoloaderProjectPaths */ public function __construct( - array $composerAutoloaderProjectPaths + private array $composerAutoloaderProjectPaths, ) { parent::__construct(); - $this->composerAutoloaderProjectPaths = $composerAutoloaderProjectPaths; } protected function configure(): void @@ -46,7 +54,6 @@ protected function configure(): void ->setDescription('(Internal) Support for parallel analysis.') ->setDefinition([ new InputArgument('paths', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Paths with source code to run analysis on'), - new InputOption('paths-file', null, InputOption::VALUE_REQUIRED, 'Path to a file with a list of paths to run analysis on'), new InputOption('configuration', 'c', InputOption::VALUE_REQUIRED, 'Path to project configuration file'), new InputOption(AnalyseCommand::OPTION_LEVEL, 'l', InputOption::VALUE_REQUIRED, 'Level of rule options - the higher the stricter'), new InputOption('autoload-file', 'a', InputOption::VALUE_REQUIRED, 'Project\'s additional autoload file path'), @@ -66,7 +73,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $autoloadFile = $input->getOption('autoload-file'); $configuration = $input->getOption('configuration'); $level = $input->getOption(AnalyseCommand::OPTION_LEVEL); - $pathsFile = $input->getOption('paths-file'); $allowXdebug = $input->getOption('xdebug'); $port = $input->getOption('port'); $identifier = $input->getOption('identifier'); @@ -77,12 +83,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int || (!is_string($autoloadFile) && $autoloadFile !== null) || (!is_string($configuration) && $configuration !== null) || (!is_string($level) && $level !== null) - || (!is_string($pathsFile) && $pathsFile !== null) || (!is_bool($allowXdebug)) || !is_string($port) || !is_string($identifier) ) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } /** @var string|null $tmpFile */ @@ -101,7 +106,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $input, $output, $paths, - $pathsFile, $memoryLimit, $autoloadFile, $this->composerAutoloaderProjectPaths, @@ -110,10 +114,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $level, $allowXdebug, false, + $singleReflectionFile, + null, false, - $singleReflectionFile ); - } catch (\PHPStan\Command\InceptionNotSuccessfulException $e) { + } catch (InceptionNotSuccessfulException $e) { return 1; } $loop = new StreamSelectLoop(); @@ -123,7 +128,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int try { [$analysedFiles] = $inceptionResult->getFiles(); $analysedFiles = $this->switchTmpFile($analysedFiles, $insteadOfFile, $tmpFile); - } catch (\PHPStan\File\PathNotFoundException $e) { + } catch (PathNotFoundException $e) { $inceptionResult->getErrorOutput()->writeLineFormatted(sprintf('%s', $e->getMessage())); return 1; } @@ -136,8 +141,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int $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 { - $out = new Encoder($connection, defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0); - $in = new Decoder($connection, true, 512, defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0, $container->getParameter('parallel')['buffer']); + // 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); }); @@ -152,13 +160,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } /** - * @param Container $container - * @param WritableStreamInterface $out - * @param ReadableStreamInterface $in - * @param OutputInterface $output * @param array $analysedFiles - * @param string|null $tmpFile - * @param string|null $insteadOfFile */ private function runWorker( Container $container, @@ -167,10 +169,10 @@ private function runWorker( OutputInterface $output, array $analysedFiles, ?string $tmpFile, - ?string $insteadOfFile + ?string $insteadOfFile, ): void { - $handleError = function (\Throwable $error) use ($out, $output): void { + $handleError = function (Throwable $error) use ($out, $output): void { $this->errorCount++; $output->writeln(sprintf('Error: %s', $error->getMessage())); $out->write([ @@ -185,14 +187,11 @@ private function runWorker( $out->end(); }; $out->on('error', $handleError); - /** @var FileAnalyser $fileAnalyser */ $fileAnalyser = $container->getByType(FileAnalyser::class); - /** @var Registry $registry */ $registry = $container->getByType(Registry::class); - - $in->on('data', function (array $json) use ($fileAnalyser, $registry, $out, $analysedFiles, $tmpFile, $insteadOfFile): void { + $in->on('data', function (array $json) use ($fileAnalyser, $registry, $out, $analysedFiles, $tmpFile, $insteadOfFile, $output): void { $action = $json['action']; if ($action !== 'analyse') { return; @@ -215,16 +214,18 @@ private function runWorker( foreach ($fileErrors as $fileError) { $errors[] = $fileError; } - } catch (\Throwable $t) { + } catch (Throwable $t) { $this->errorCount++; $internalErrorsCount++; $internalErrorMessage = sprintf('Internal error: %s in file %s', $t->getMessage(), $file); - $internalErrorMessage .= sprintf( - '%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' - ); + + $bugReportUrl = 'https://github.com/phpstan/phpstan/issues/new?template=Bug_report.md'; + if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { + $internalErrorMessage .= sprintf('%sPost the following stack trace to %s: %s%s', "\n\n", $bugReportUrl, "\n", $t->getTraceAsString()); + } else { + $internalErrorMessage .= sprintf('%sRun PHPStan with -v option and post the stack trace to:%s%s', "\n", "\n", $bugReportUrl); + } + $errors[] = $internalErrorMessage; } } @@ -244,14 +245,12 @@ private function runWorker( /** * @param string[] $analysedFiles - * @param string|null $insteadOfFile - * @param string|null $tmpFile * @return string[] */ private function switchTmpFile( array $analysedFiles, ?string $insteadOfFile, - ?string $tmpFile + ?string $tmpFile, ): array { $analysedFiles = array_values(array_filter($analysedFiles, static function (string $file) use ($insteadOfFile): bool { diff --git a/src/Dependency/DependencyDumper.php b/src/Dependency/DependencyDumper.php deleted file mode 100644 index c7480dc01a..0000000000 --- a/src/Dependency/DependencyDumper.php +++ /dev/null @@ -1,97 +0,0 @@ -dependencyResolver = $dependencyResolver; - $this->nodeScopeResolver = $nodeScopeResolver; - $this->parser = $parser; - $this->scopeFactory = $scopeFactory; - $this->fileFinder = $fileFinder; - } - - /** - * @param string[] $files - * @param callable(int $count): void $countCallback - * @param callable(): void $progressCallback - * @param string[]|null $analysedPaths - * @return string[][] - */ - public function dumpDependencies( - array $files, - callable $countCallback, - callable $progressCallback, - ?array $analysedPaths - ): array - { - $analysedFiles = $files; - if ($analysedPaths !== null) { - $analysedFiles = $this->fileFinder->findFiles($analysedPaths)->getFiles(); - } - $this->nodeScopeResolver->setAnalysedFiles($analysedFiles); - $analysedFiles = array_fill_keys($analysedFiles, true); - - $dependencies = []; - $countCallback(count($files)); - foreach ($files as $file) { - try { - $parserNodes = $this->parser->parseFile($file); - } catch (\PHPStan\Parser\ParserErrorsException $e) { - continue; - } - - $fileDependencies = []; - try { - $this->nodeScopeResolver->processNodes( - $parserNodes, - $this->scopeFactory->create(ScopeContext::create($file)), - function (\PhpParser\Node $node, Scope $scope) use ($analysedFiles, &$fileDependencies): void { - $dependencies = $this->dependencyResolver->resolveDependencies($node, $scope); - $fileDependencies = array_merge( - $fileDependencies, - $dependencies->getFileDependencies($scope->getFile(), $analysedFiles) - ); - } - ); - } catch (\PHPStan\AnalysedCodeException $e) { - // pass - } - - foreach (array_unique($fileDependencies) as $fileDependency) { - $dependencies[$fileDependency][] = $file; - } - - $progressCallback(); - } - - return $dependencies; - } - -} diff --git a/src/Dependency/DependencyResolver.php b/src/Dependency/DependencyResolver.php index 3580e529cd..8c650dd8b3 100644 --- a/src/Dependency/DependencyResolver.php +++ b/src/Dependency/DependencyResolver.php @@ -2,64 +2,65 @@ namespace PHPStan\Dependency; +use PhpParser\Node; use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\ArrayDimFetch; use PhpParser\Node\Expr\Closure; use PhpParser\Node\Name; use PhpParser\Node\Stmt\Foreach_; use PHPStan\Analyser\Scope; +use PHPStan\Broker\ClassNotFoundException; +use PHPStan\Broker\FunctionNotFoundException; use PHPStan\File\FileHelper; use PHPStan\Node\InClassMethodNode; use PHPStan\Node\InFunctionNode; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Reflection\ReflectionWithFilename; use PHPStan\Type\ClosureType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Type; +use function array_merge; +use function count; class DependencyResolver { - private FileHelper $fileHelper; - - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private ExportedNodeResolver $exportedNodeResolver; - public function __construct( - FileHelper $fileHelper, - ReflectionProvider $reflectionProvider, - ExportedNodeResolver $exportedNodeResolver + private FileHelper $fileHelper, + private ReflectionProvider $reflectionProvider, + private ExportedNodeResolver $exportedNodeResolver, ) { - $this->fileHelper = $fileHelper; - $this->reflectionProvider = $reflectionProvider; - $this->exportedNodeResolver = $exportedNodeResolver; } - public function resolveDependencies(\PhpParser\Node $node, Scope $scope): NodeDependencies + public function resolveDependencies(Node $node, Scope $scope): NodeDependencies { $dependenciesReflections = []; - if ($node instanceof \PhpParser\Node\Stmt\Class_) { + if ($node instanceof Node\Stmt\Class_) { if ($node->extends !== null) { $this->addClassToDependencies($node->extends->toString(), $dependenciesReflections); } foreach ($node->implements as $className) { $this->addClassToDependencies($className->toString(), $dependenciesReflections); } - } elseif ($node instanceof \PhpParser\Node\Stmt\Interface_) { + } elseif ($node instanceof Node\Stmt\Interface_) { foreach ($node->extends as $className) { $this->addClassToDependencies($className->toString(), $dependenciesReflections); } + } elseif ($node instanceof Node\Stmt\Enum_) { + foreach ($node->implements as $className) { + $this->addClassToDependencies($className->toString(), $dependenciesReflections); + } } elseif ($node instanceof InClassMethodNode) { $nativeMethod = $scope->getFunction(); if ($nativeMethod !== null) { $parametersAcceptor = ParametersAcceptorSelector::selectSingle($nativeMethod->getVariants()); $this->extractThrowType($nativeMethod->getThrowType(), $dependenciesReflections); - if ($parametersAcceptor instanceof \PHPStan\Reflection\ParametersAcceptorWithPhpDocs) { + if ($parametersAcceptor instanceof ParametersAcceptorWithPhpDocs) { $this->extractFromParametersAcceptor($parametersAcceptor, $dependenciesReflections); } } @@ -87,12 +88,12 @@ public function resolveDependencies(\PhpParser\Node $node, Scope $scope): NodeDe foreach ($returnTypeReferencedClasses as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } - } elseif ($node instanceof \PhpParser\Node\Expr\FuncCall) { + } elseif ($node instanceof Node\Expr\FuncCall) { $functionName = $node->name; - if ($functionName instanceof \PhpParser\Node\Name) { + if ($functionName instanceof Node\Name) { try { $dependenciesReflections[] = $this->getFunctionReflection($functionName, $scope); - } catch (\PHPStan\Broker\FunctionNotFoundException $e) { + } catch (FunctionNotFoundException) { // pass } } else { @@ -112,7 +113,7 @@ public function resolveDependencies(\PhpParser\Node $node, Scope $scope): NodeDe foreach ($returnType->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } - } elseif ($node instanceof \PhpParser\Node\Expr\MethodCall || $node instanceof \PhpParser\Node\Expr\PropertyFetch) { + } elseif ($node instanceof Node\Expr\MethodCall || $node instanceof Node\Expr\PropertyFetch) { $classNames = $scope->getType($node->var)->getReferencedClasses(); foreach ($classNames as $className) { $this->addClassToDependencies($className, $dependenciesReflections); @@ -123,11 +124,11 @@ public function resolveDependencies(\PhpParser\Node $node, Scope $scope): NodeDe $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } elseif ( - $node instanceof \PhpParser\Node\Expr\StaticCall - || $node instanceof \PhpParser\Node\Expr\ClassConstFetch - || $node instanceof \PhpParser\Node\Expr\StaticPropertyFetch + $node instanceof Node\Expr\StaticCall + || $node instanceof Node\Expr\ClassConstFetch + || $node instanceof Node\Expr\StaticPropertyFetch ) { - if ($node->class instanceof \PhpParser\Node\Name) { + if ($node->class instanceof Node\Name) { $this->addClassToDependencies($scope->resolveName($node->class), $dependenciesReflections); } else { foreach ($scope->getType($node->class)->getReferencedClasses() as $referencedClass) { @@ -140,19 +141,19 @@ public function resolveDependencies(\PhpParser\Node $node, Scope $scope): NodeDe $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } elseif ( - $node instanceof \PhpParser\Node\Expr\New_ - && $node->class instanceof \PhpParser\Node\Name + $node instanceof Node\Expr\New_ + && $node->class instanceof Node\Name ) { $this->addClassToDependencies($scope->resolveName($node->class), $dependenciesReflections); - } elseif ($node instanceof \PhpParser\Node\Stmt\TraitUse) { + } elseif ($node instanceof Node\Stmt\TraitUse) { foreach ($node->traits as $traitName) { $this->addClassToDependencies($traitName->toString(), $dependenciesReflections); } - } elseif ($node instanceof \PhpParser\Node\Expr\Instanceof_) { + } elseif ($node instanceof Node\Expr\Instanceof_) { if ($node->class instanceof Name) { $this->addClassToDependencies($scope->resolveName($node->class), $dependenciesReflections); } - } elseif ($node instanceof \PhpParser\Node\Stmt\Catch_) { + } elseif ($node instanceof Node\Stmt\Catch_) { foreach ($node->types as $type) { $this->addClassToDependencies($scope->resolveName($type), $dependenciesReflections); } @@ -194,27 +195,31 @@ public function resolveDependencies(\PhpParser\Node $node, Scope $scope): NodeDe private function considerArrayForCallableTest(Scope $scope, Array_ $arrayNode): bool { - if (!isset($arrayNode->items[0])) { + $items = $arrayNode->items; + if (count($items) !== 2) { return false; } - $itemType = $scope->getType($arrayNode->items[0]->value); + if ($items[0] === null) { + return false; + } + + $itemType = $scope->getType($items[0]->value); if (!$itemType instanceof ConstantStringType) { - return true; + return false; } return $itemType->isClassString(); } /** - * @param string $className - * @param array $dependenciesReflections + * @param array $dependenciesReflections */ private function addClassToDependencies(string $className, array &$dependenciesReflections): void { try { $classReflection = $this->reflectionProvider->getClass($className); - } catch (\PHPStan\Broker\ClassNotFoundException $e) { + } catch (ClassNotFoundException) { return; } @@ -230,32 +235,26 @@ private function addClassToDependencies(string $className, array &$dependenciesR } $classReflection = $classReflection->getParentClass(); - } while ($classReflection !== false); + } while ($classReflection !== null); } - private function getFunctionReflection(\PhpParser\Node\Name $nameNode, ?Scope $scope): ReflectionWithFilename + private function getFunctionReflection(Node\Name $nameNode, ?Scope $scope): FunctionReflection { - $reflection = $this->reflectionProvider->getFunction($nameNode, $scope); - if (!$reflection instanceof ReflectionWithFilename) { - throw new \PHPStan\Broker\FunctionNotFoundException((string) $nameNode); - } - - return $reflection; + return $this->reflectionProvider->getFunction($nameNode, $scope); } /** - * @param ParametersAcceptorWithPhpDocs $parametersAcceptor - * @param ReflectionWithFilename[] $dependenciesReflections + * @param array $dependenciesReflections */ private function extractFromParametersAcceptor( ParametersAcceptorWithPhpDocs $parametersAcceptor, - array &$dependenciesReflections + array &$dependenciesReflections, ): void { foreach ($parametersAcceptor->getParameters() as $parameter) { $referencedClasses = array_merge( $parameter->getNativeType()->getReferencedClasses(), - $parameter->getPhpDocType()->getReferencedClasses() + $parameter->getPhpDocType()->getReferencedClasses(), ); foreach ($referencedClasses as $referencedClass) { @@ -265,7 +264,7 @@ private function extractFromParametersAcceptor( $returnTypeReferencedClasses = array_merge( $parametersAcceptor->getNativeReturnType()->getReferencedClasses(), - $parametersAcceptor->getPhpDocReturnType()->getReferencedClasses() + $parametersAcceptor->getPhpDocReturnType()->getReferencedClasses(), ); foreach ($returnTypeReferencedClasses as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); @@ -273,12 +272,11 @@ private function extractFromParametersAcceptor( } /** - * @param Type|null $throwType - * @param ReflectionWithFilename[] $dependenciesReflections + * @param array $dependenciesReflections */ private function extractThrowType( ?Type $throwType, - array &$dependenciesReflections + array &$dependenciesReflections, ): void { if ($throwType === null) { diff --git a/src/Dependency/ExportedNode.php b/src/Dependency/ExportedNode.php index ee088cd05b..59c32f2a0c 100644 --- a/src/Dependency/ExportedNode.php +++ b/src/Dependency/ExportedNode.php @@ -9,13 +9,11 @@ public function equals(self $node): bool; /** * @param mixed[] $properties - * @return self */ public static function __set_state(array $properties): self; /** * @param mixed[] $data - * @return self */ public static function decode(array $data): self; diff --git a/src/Dependency/ExportedNode/ExportedClassConstantNode.php b/src/Dependency/ExportedNode/ExportedClassConstantNode.php index 4e75697de7..c24510b3cc 100644 --- a/src/Dependency/ExportedNode/ExportedClassConstantNode.php +++ b/src/Dependency/ExportedNode/ExportedClassConstantNode.php @@ -4,24 +4,13 @@ use JsonSerializable; use PHPStan\Dependency\ExportedNode; +use ReturnTypeWillChange; class ExportedClassConstantNode implements ExportedNode, JsonSerializable { - private string $name; - - private string $value; - - private bool $public; - - private bool $private; - - public function __construct(string $name, string $value, bool $public, bool $private) + public function __construct(private string $name, private string $value) { - $this->name = $name; - $this->value = $value; - $this->public = $public; - $this->private = $private; } public function equals(ExportedNode $node): bool @@ -31,9 +20,7 @@ public function equals(ExportedNode $node): bool } return $this->name === $node->name - && $this->value === $node->value - && $this->public === $node->public - && $this->private === $node->private; + && $this->value === $node->value; } /** @@ -45,8 +32,6 @@ public static function __set_state(array $properties): ExportedNode return new self( $properties['name'], $properties['value'], - $properties['public'], - $properties['private'] ); } @@ -59,14 +44,13 @@ public static function decode(array $data): ExportedNode return new self( $data['name'], $data['value'], - $data['public'], - $data['private'] ); } /** * @return mixed */ + #[ReturnTypeWillChange] public function jsonSerialize() { return [ @@ -74,8 +58,6 @@ public function jsonSerialize() 'data' => [ 'name' => $this->name, 'value' => $this->value, - 'public' => $this->public, - 'private' => $this->private, ], ]; } diff --git a/src/Dependency/ExportedNode/ExportedClassConstantsNode.php b/src/Dependency/ExportedNode/ExportedClassConstantsNode.php new file mode 100644 index 0000000000..04bace7282 --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedClassConstantsNode.php @@ -0,0 +1,108 @@ +phpDoc === null) { + if ($node->phpDoc !== null) { + return false; + } + } elseif ($node->phpDoc !== null) { + if (!$this->phpDoc->equals($node->phpDoc)) { + return false; + } + } else { + return false; + } + + if (count($this->constants) !== count($node->constants)) { + return false; + } + + foreach ($this->constants as $i => $constant) { + if (!$constant->equals($node->constants[$i])) { + return false; + } + } + + return $this->public === $node->public + && $this->private === $node->private + && $this->final === $node->final; + } + + /** + * @param mixed[] $properties + * @return self + */ + public static function __set_state(array $properties): ExportedNode + { + return new self( + $properties['constants'], + $properties['public'], + $properties['private'], + $properties['final'], + $properties['phpDoc'], + ); + } + + /** + * @param mixed[] $data + * @return self + */ + public static function decode(array $data): ExportedNode + { + return new self( + array_map(static function (array $constantData): ExportedClassConstantNode { + if ($constantData['type'] !== ExportedClassConstantNode::class) { + throw new ShouldNotHappenException(); + } + return ExportedClassConstantNode::decode($constantData['data']); + }, $data['constants']), + $data['public'], + $data['private'], + $data['final'], + $data['phpDoc'] !== null ? ExportedPhpDocNode::decode($data['phpDoc']['data']) : null, + ); + } + + /** + * @return mixed + */ + #[ReturnTypeWillChange] + public function jsonSerialize() + { + return [ + 'type' => self::class, + 'data' => [ + 'constants' => $this->constants, + 'public' => $this->public, + 'private' => $this->private, + 'final' => $this->final, + 'phpDoc' => $this->phpDoc, + ], + ]; + } + +} diff --git a/src/Dependency/ExportedNode/ExportedClassNode.php b/src/Dependency/ExportedNode/ExportedClassNode.php index 4977a47643..c46001f515 100644 --- a/src/Dependency/ExportedNode/ExportedClassNode.php +++ b/src/Dependency/ExportedNode/ExportedClassNode.php @@ -4,58 +4,32 @@ use JsonSerializable; use PHPStan\Dependency\ExportedNode; +use PHPStan\ShouldNotHappenException; +use ReturnTypeWillChange; +use function array_map; +use function count; class ExportedClassNode implements ExportedNode, JsonSerializable { - private string $name; - - private ?ExportedPhpDocNode $phpDoc; - - private bool $abstract; - - private bool $final; - - private ?string $extends; - - /** @var string[] */ - private array $implements; - - /** @var string[] */ - private array $usedTraits; - - /** @var ExportedTraitUseAdaptation[] */ - private array $traitUseAdaptations; - /** - * @param string $name - * @param ExportedPhpDocNode|null $phpDoc - * @param bool $abstract - * @param bool $final - * @param string|null $extends * @param string[] $implements * @param string[] $usedTraits * @param ExportedTraitUseAdaptation[] $traitUseAdaptations + * @param ExportedNode[] $statements */ public function __construct( - string $name, - ?ExportedPhpDocNode $phpDoc, - bool $abstract, - bool $final, - ?string $extends, - array $implements, - array $usedTraits, - array $traitUseAdaptations + private string $name, + private ?ExportedPhpDocNode $phpDoc, + private bool $abstract, + private bool $final, + private ?string $extends, + private array $implements, + private array $usedTraits, + private array $traitUseAdaptations, + private array $statements, ) { - $this->name = $name; - $this->phpDoc = $phpDoc; - $this->abstract = $abstract; - $this->final = $final; - $this->extends = $extends; - $this->implements = $implements; - $this->usedTraits = $usedTraits; - $this->traitUseAdaptations = $traitUseAdaptations; } public function equals(ExportedNode $node): bool @@ -87,6 +61,18 @@ public function equals(ExportedNode $node): bool } } + if (count($this->statements) !== count($node->statements)) { + return false; + } + + foreach ($this->statements as $i => $statement) { + if ($statement->equals($node->statements[$i])) { + continue; + } + + return false; + } + return $this->name === $node->name && $this->abstract === $node->abstract && $this->final === $node->final @@ -109,13 +95,15 @@ public static function __set_state(array $properties): ExportedNode $properties['extends'], $properties['implements'], $properties['usedTraits'], - $properties['traitUseAdaptations'] + $properties['traitUseAdaptations'], + $properties['statements'], ); } /** * @return mixed */ + #[ReturnTypeWillChange] public function jsonSerialize() { return [ @@ -129,6 +117,7 @@ public function jsonSerialize() 'implements' => $this->implements, 'usedTraits' => $this->usedTraits, 'traitUseAdaptations' => $this->traitUseAdaptations, + 'statements' => $this->statements, ], ]; } @@ -149,10 +138,15 @@ public static function decode(array $data): ExportedNode $data['usedTraits'], array_map(static function (array $traitUseAdaptationData): ExportedTraitUseAdaptation { if ($traitUseAdaptationData['type'] !== ExportedTraitUseAdaptation::class) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return ExportedTraitUseAdaptation::decode($traitUseAdaptationData['data']); - }, $data['traitUseAdaptations']) + }, $data['traitUseAdaptations']), + array_map(static function (array $node): ExportedNode { + $nodeType = $node['type']; + + return $nodeType::decode($node['data']); + }, $data['statements']), ); } diff --git a/src/Dependency/ExportedNode/ExportedEnumCaseNode.php b/src/Dependency/ExportedNode/ExportedEnumCaseNode.php new file mode 100644 index 0000000000..da304b5893 --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedEnumCaseNode.php @@ -0,0 +1,80 @@ +phpDoc === null) { + if ($node->phpDoc !== null) { + return false; + } + } elseif ($node->phpDoc !== null) { + if (!$this->phpDoc->equals($node->phpDoc)) { + return false; + } + } else { + return false; + } + + return $this->name === $node->name + && $this->value === $node->value; + } + + /** + * @param mixed[] $properties + * @return self + */ + public static function __set_state(array $properties): ExportedNode + { + return new self( + $properties['name'], + $properties['value'], + $properties['phpDoc'], + ); + } + + /** + * @param mixed[] $data + * @return self + */ + public static function decode(array $data): ExportedNode + { + return new self( + $data['name'], + $data['value'], + $data['phpDoc'] !== null ? ExportedPhpDocNode::decode($data['phpDoc']['data']) : null, + ); + } + + /** + * @return mixed + */ + #[ReturnTypeWillChange] + public function jsonSerialize() + { + return [ + 'type' => self::class, + 'data' => [ + 'name' => $this->name, + 'value' => $this->value, + 'phpDoc' => $this->phpDoc, + ], + ]; + } + +} diff --git a/src/Dependency/ExportedNode/ExportedEnumNode.php b/src/Dependency/ExportedNode/ExportedEnumNode.php new file mode 100644 index 0000000000..831a823658 --- /dev/null +++ b/src/Dependency/ExportedNode/ExportedEnumNode.php @@ -0,0 +1,109 @@ +phpDoc === null) { + if ($node->phpDoc !== null) { + return false; + } + } elseif ($node->phpDoc !== null) { + if (!$this->phpDoc->equals($node->phpDoc)) { + return false; + } + } else { + return false; + } + + if (count($this->statements) !== count($node->statements)) { + return false; + } + + foreach ($this->statements as $i => $statement) { + if ($statement->equals($node->statements[$i])) { + continue; + } + + return false; + } + + return $this->name === $node->name + && $this->scalarType === $node->scalarType + && $this->implements === $node->implements; + } + + /** + * @param mixed[] $properties + * @return self + */ + public static function __set_state(array $properties): ExportedNode + { + return new self( + $properties['name'], + $properties['scalarType'], + $properties['phpDoc'], + $properties['implements'], + $properties['statements'], + ); + } + + /** + * @return mixed + */ + #[ReturnTypeWillChange] + public function jsonSerialize() + { + return [ + 'type' => self::class, + 'data' => [ + 'name' => $this->name, + 'scalarType' => $this->scalarType, + 'phpDoc' => $this->phpDoc, + 'implements' => $this->implements, + 'statements' => $this->statements, + ], + ]; + } + + /** + * @param mixed[] $data + * @return self + */ + public static function decode(array $data): ExportedNode + { + return new self( + $data['name'], + $data['scalarType'], + $data['phpDoc'] !== null ? ExportedPhpDocNode::decode($data['phpDoc']['data']) : null, + $data['implements'], + array_map(static function (array $node): ExportedNode { + $nodeType = $node['type']; + + return $nodeType::decode($node['data']); + }, $data['statements']), + ); + } + +} diff --git a/src/Dependency/ExportedNode/ExportedFunctionNode.php b/src/Dependency/ExportedNode/ExportedFunctionNode.php index 6c4382b1b0..58f2704c79 100644 --- a/src/Dependency/ExportedNode/ExportedFunctionNode.php +++ b/src/Dependency/ExportedNode/ExportedFunctionNode.php @@ -4,41 +4,25 @@ use JsonSerializable; use PHPStan\Dependency\ExportedNode; +use PHPStan\ShouldNotHappenException; +use ReturnTypeWillChange; +use function array_map; +use function count; class ExportedFunctionNode implements ExportedNode, JsonSerializable { - private string $name; - - private ?ExportedPhpDocNode $phpDoc; - - private bool $byRef; - - private ?string $returnType; - - /** @var ExportedParameterNode[] */ - private array $parameters; - /** - * @param string $name - * @param ExportedPhpDocNode|null $phpDoc - * @param bool $byRef - * @param string|null $returnType * @param ExportedParameterNode[] $parameters */ public function __construct( - string $name, - ?ExportedPhpDocNode $phpDoc, - bool $byRef, - ?string $returnType, - array $parameters + private string $name, + private ?ExportedPhpDocNode $phpDoc, + private bool $byRef, + private ?string $returnType, + private array $parameters, ) { - $this->name = $name; - $this->phpDoc = $phpDoc; - $this->byRef = $byRef; - $this->returnType = $returnType; - $this->parameters = $parameters; } public function equals(ExportedNode $node): bool @@ -86,13 +70,14 @@ public static function __set_state(array $properties): ExportedNode $properties['phpDoc'], $properties['byRef'], $properties['returnType'], - $properties['parameters'] + $properties['parameters'], ); } /** * @return mixed */ + #[ReturnTypeWillChange] public function jsonSerialize() { return [ @@ -120,10 +105,10 @@ public static function decode(array $data): ExportedNode $data['returnType'], array_map(static function (array $parameterData): ExportedParameterNode { if ($parameterData['type'] !== ExportedParameterNode::class) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return ExportedParameterNode::decode($parameterData['data']); - }, $data['parameters']) + }, $data['parameters']), ); } diff --git a/src/Dependency/ExportedNode/ExportedInterfaceNode.php b/src/Dependency/ExportedNode/ExportedInterfaceNode.php index 0519efba99..285db67f87 100644 --- a/src/Dependency/ExportedNode/ExportedInterfaceNode.php +++ b/src/Dependency/ExportedNode/ExportedInterfaceNode.php @@ -4,27 +4,19 @@ use JsonSerializable; use PHPStan\Dependency\ExportedNode; +use ReturnTypeWillChange; +use function array_map; +use function count; class ExportedInterfaceNode implements ExportedNode, JsonSerializable { - private string $name; - - private ?ExportedPhpDocNode $phpDoc; - - /** @var string[] */ - private array $extends; - /** - * @param string $name - * @param ExportedPhpDocNode|null $phpDoc * @param string[] $extends + * @param ExportedNode[] $statements */ - public function __construct(string $name, ?ExportedPhpDocNode $phpDoc, array $extends) + public function __construct(private string $name, private ?ExportedPhpDocNode $phpDoc, private array $extends, private array $statements) { - $this->name = $name; - $this->phpDoc = $phpDoc; - $this->extends = $extends; } public function equals(ExportedNode $node): bool @@ -45,6 +37,18 @@ public function equals(ExportedNode $node): bool return false; } + if (count($this->statements) !== count($node->statements)) { + return false; + } + + foreach ($this->statements as $i => $statement) { + if ($statement->equals($node->statements[$i])) { + continue; + } + + return false; + } + return $this->name === $node->name && $this->extends === $node->extends; } @@ -58,13 +62,15 @@ public static function __set_state(array $properties): ExportedNode return new self( $properties['name'], $properties['phpDoc'], - $properties['extends'] + $properties['extends'], + $properties['statements'], ); } /** * @return mixed */ + #[ReturnTypeWillChange] public function jsonSerialize() { return [ @@ -73,6 +79,7 @@ public function jsonSerialize() 'name' => $this->name, 'phpDoc' => $this->phpDoc, 'extends' => $this->extends, + 'statements' => $this->statements, ], ]; } @@ -86,7 +93,12 @@ public static function decode(array $data): ExportedNode return new self( $data['name'], $data['phpDoc'] !== null ? ExportedPhpDocNode::decode($data['phpDoc']['data']) : null, - $data['extends'] + $data['extends'], + array_map(static function (array $node): ExportedNode { + $nodeType = $node['type']; + + return $nodeType::decode($node['data']); + }, $data['statements']), ); } diff --git a/src/Dependency/ExportedNode/ExportedMethodNode.php b/src/Dependency/ExportedNode/ExportedMethodNode.php index a59a40c25a..7cb3376fab 100644 --- a/src/Dependency/ExportedNode/ExportedMethodNode.php +++ b/src/Dependency/ExportedNode/ExportedMethodNode.php @@ -4,66 +4,30 @@ use JsonSerializable; use PHPStan\Dependency\ExportedNode; +use PHPStan\ShouldNotHappenException; +use ReturnTypeWillChange; +use function array_map; +use function count; class ExportedMethodNode implements ExportedNode, JsonSerializable { - private string $name; - - private ?ExportedPhpDocNode $phpDoc; - - private bool $byRef; - - private bool $public; - - private bool $private; - - private bool $abstract; - - private bool $final; - - private bool $static; - - private ?string $returnType; - - /** @var ExportedParameterNode[] */ - private array $parameters; - /** - * @param string $name - * @param ExportedPhpDocNode|null $phpDoc - * @param bool $byRef - * @param bool $public - * @param bool $private - * @param bool $abstract - * @param bool $final - * @param bool $static - * @param string|null $returnType * @param ExportedParameterNode[] $parameters */ public function __construct( - string $name, - ?ExportedPhpDocNode $phpDoc, - bool $byRef, - bool $public, - bool $private, - bool $abstract, - bool $final, - bool $static, - ?string $returnType, - array $parameters + private string $name, + private ?ExportedPhpDocNode $phpDoc, + private bool $byRef, + private bool $public, + private bool $private, + private bool $abstract, + private bool $final, + private bool $static, + private ?string $returnType, + private array $parameters, ) { - $this->name = $name; - $this->phpDoc = $phpDoc; - $this->byRef = $byRef; - $this->public = $public; - $this->private = $private; - $this->abstract = $abstract; - $this->final = $final; - $this->static = $static; - $this->returnType = $returnType; - $this->parameters = $parameters; } public function equals(ExportedNode $node): bool @@ -121,13 +85,14 @@ public static function __set_state(array $properties): ExportedNode $properties['final'], $properties['static'], $properties['returnType'], - $properties['parameters'] + $properties['parameters'], ); } /** * @return mixed */ + #[ReturnTypeWillChange] public function jsonSerialize() { return [ @@ -165,10 +130,10 @@ public static function decode(array $data): ExportedNode $data['returnType'], array_map(static function (array $parameterData): ExportedParameterNode { if ($parameterData['type'] !== ExportedParameterNode::class) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return ExportedParameterNode::decode($parameterData['data']); - }, $data['parameters']) + }, $data['parameters']), ); } diff --git a/src/Dependency/ExportedNode/ExportedParameterNode.php b/src/Dependency/ExportedNode/ExportedParameterNode.php index 5f171a442f..ad36f20c42 100644 --- a/src/Dependency/ExportedNode/ExportedParameterNode.php +++ b/src/Dependency/ExportedNode/ExportedParameterNode.php @@ -4,33 +4,19 @@ use JsonSerializable; use PHPStan\Dependency\ExportedNode; +use ReturnTypeWillChange; class ExportedParameterNode implements ExportedNode, JsonSerializable { - private string $name; - - private ?string $type; - - private bool $byRef; - - private bool $variadic; - - private bool $hasDefault; - public function __construct( - string $name, - ?string $type, - bool $byRef, - bool $variadic, - bool $hasDefault + private string $name, + private ?string $type, + private bool $byRef, + private bool $variadic, + private bool $hasDefault, ) { - $this->name = $name; - $this->type = $type; - $this->byRef = $byRef; - $this->variadic = $variadic; - $this->hasDefault = $hasDefault; } public function equals(ExportedNode $node): bool @@ -57,13 +43,14 @@ public static function __set_state(array $properties): ExportedNode $properties['type'], $properties['byRef'], $properties['variadic'], - $properties['hasDefault'] + $properties['hasDefault'], ); } /** * @return mixed */ + #[ReturnTypeWillChange] public function jsonSerialize() { return [ @@ -89,7 +76,7 @@ public static function decode(array $data): ExportedNode $data['type'], $data['byRef'], $data['variadic'], - $data['hasDefault'] + $data['hasDefault'], ); } diff --git a/src/Dependency/ExportedNode/ExportedPhpDocNode.php b/src/Dependency/ExportedNode/ExportedPhpDocNode.php index e271b6b752..fbb3d3bd0d 100644 --- a/src/Dependency/ExportedNode/ExportedPhpDocNode.php +++ b/src/Dependency/ExportedNode/ExportedPhpDocNode.php @@ -4,26 +4,19 @@ use JsonSerializable; use PHPStan\Dependency\ExportedNode; +use ReturnTypeWillChange; class ExportedPhpDocNode implements ExportedNode, JsonSerializable { - private string $phpDocString; - - private ?string $namespace; - /** @var array alias(string) => fullName(string) */ private array $uses; /** - * @param string $phpDocString - * @param string|null $namespace * @param array $uses */ - public function __construct(string $phpDocString, ?string $namespace, array $uses) + public function __construct(private string $phpDocString, private ?string $namespace, array $uses) { - $this->phpDocString = $phpDocString; - $this->namespace = $namespace; $this->uses = $uses; } @@ -41,6 +34,7 @@ public function equals(ExportedNode $node): bool /** * @return mixed */ + #[ReturnTypeWillChange] public function jsonSerialize() { return [ diff --git a/src/Dependency/ExportedNode/ExportedPropertyNode.php b/src/Dependency/ExportedNode/ExportedPropertiesNode.php similarity index 62% rename from src/Dependency/ExportedNode/ExportedPropertyNode.php rename to src/Dependency/ExportedNode/ExportedPropertiesNode.php index 37aee5a044..2b61556824 100644 --- a/src/Dependency/ExportedNode/ExportedPropertyNode.php +++ b/src/Dependency/ExportedNode/ExportedPropertiesNode.php @@ -4,37 +4,25 @@ use JsonSerializable; use PHPStan\Dependency\ExportedNode; +use ReturnTypeWillChange; +use function count; -class ExportedPropertyNode implements JsonSerializable, ExportedNode +class ExportedPropertiesNode implements JsonSerializable, ExportedNode { - private string $name; - - private ?ExportedPhpDocNode $phpDoc; - - private ?string $type; - - private bool $public; - - private bool $private; - - private bool $static; - + /** + * @param string[] $names + */ public function __construct( - string $name, - ?ExportedPhpDocNode $phpDoc, - ?string $type, - bool $public, - bool $private, - bool $static + private array $names, + private ?ExportedPhpDocNode $phpDoc, + private ?string $type, + private bool $public, + private bool $private, + private bool $static, + private bool $readonly, ) { - $this->name = $name; - $this->phpDoc = $phpDoc; - $this->type = $type; - $this->public = $public; - $this->private = $private; - $this->static = $static; } public function equals(ExportedNode $node): bool @@ -55,11 +43,21 @@ public function equals(ExportedNode $node): bool return false; } - return $this->name === $node->name - && $this->type === $node->type + if (count($this->names) !== count($node->names)) { + return false; + } + + foreach ($this->names as $i => $name) { + if ($name !== $node->names[$i]) { + return false; + } + } + + return $this->type === $node->type && $this->public === $node->public && $this->private === $node->private - && $this->static === $node->static; + && $this->static === $node->static + && $this->readonly === $node->readonly; } /** @@ -69,12 +67,13 @@ public function equals(ExportedNode $node): bool public static function __set_state(array $properties): ExportedNode { return new self( - $properties['name'], + $properties['names'], $properties['phpDoc'], $properties['type'], $properties['public'], $properties['private'], - $properties['static'] + $properties['static'], + $properties['readonly'], ); } @@ -85,29 +84,32 @@ public static function __set_state(array $properties): ExportedNode public static function decode(array $data): ExportedNode { return new self( - $data['name'], + $data['names'], $data['phpDoc'] !== null ? ExportedPhpDocNode::decode($data['phpDoc']['data']) : null, $data['type'], $data['public'], $data['private'], - $data['static'] + $data['static'], + $data['readonly'], ); } /** * @return mixed */ + #[ReturnTypeWillChange] public function jsonSerialize() { return [ 'type' => self::class, 'data' => [ - 'name' => $this->name, + 'names' => $this->names, 'phpDoc' => $this->phpDoc, 'type' => $this->type, 'public' => $this->public, 'private' => $this->private, 'static' => $this->static, + 'readonly' => $this->readonly, ], ]; } diff --git a/src/Dependency/ExportedNode/ExportedTraitNode.php b/src/Dependency/ExportedNode/ExportedTraitNode.php index ed8a6fa5ef..7874fca68c 100644 --- a/src/Dependency/ExportedNode/ExportedTraitNode.php +++ b/src/Dependency/ExportedNode/ExportedTraitNode.php @@ -4,15 +4,13 @@ use JsonSerializable; use PHPStan\Dependency\ExportedNode; +use ReturnTypeWillChange; class ExportedTraitNode implements ExportedNode, JsonSerializable { - private string $traitName; - - public function __construct(string $traitName) + public function __construct(private string $traitName) { - $this->traitName = $traitName; } public function equals(ExportedNode $node): bool @@ -41,6 +39,7 @@ public static function decode(array $data): ExportedNode /** * @return mixed */ + #[ReturnTypeWillChange] public function jsonSerialize() { return [ diff --git a/src/Dependency/ExportedNode/ExportedTraitUseAdaptation.php b/src/Dependency/ExportedNode/ExportedTraitUseAdaptation.php index 1de62fcf0a..57f3491010 100644 --- a/src/Dependency/ExportedNode/ExportedTraitUseAdaptation.php +++ b/src/Dependency/ExportedNode/ExportedTraitUseAdaptation.php @@ -4,63 +4,41 @@ use JsonSerializable; use PHPStan\Dependency\ExportedNode; +use ReturnTypeWillChange; class ExportedTraitUseAdaptation implements ExportedNode, JsonSerializable { - private ?string $traitName; - - private string $method; - - private ?int $newModifier; - - private ?string $newName; - - /** @var string[]|null */ - private ?array $insteadOfs; - /** - * @param string|null $traitName - * @param string $method - * @param int|null $newModifier - * @param string|null $newName * @param string[]|null $insteadOfs */ private function __construct( - ?string $traitName, - string $method, - ?int $newModifier, - ?string $newName, - ?array $insteadOfs + private ?string $traitName, + private string $method, + private ?int $newModifier, + private ?string $newName, + private ?array $insteadOfs, ) { - $this->traitName = $traitName; - $this->method = $method; - $this->newModifier = $newModifier; - $this->newName = $newName; - $this->insteadOfs = $insteadOfs; } public static function createAlias( ?string $traitName, string $method, ?int $newModifier, - ?string $newName + ?string $newName, ): self { return new self($traitName, $method, $newModifier, $newName, null); } /** - * @param string|null $traitName - * @param string $method * @param string[] $insteadOfs - * @return self */ public static function createPrecedence( ?string $traitName, string $method, - array $insteadOfs + array $insteadOfs, ): self { return new self($traitName, $method, null, null, $insteadOfs); @@ -90,7 +68,7 @@ public static function __set_state(array $properties): ExportedNode $properties['method'], $properties['newModifier'], $properties['newName'], - $properties['insteadOfs'] + $properties['insteadOfs'], ); } @@ -105,13 +83,14 @@ public static function decode(array $data): ExportedNode $data['method'], $data['newModifier'], $data['newName'], - $data['insteadOfs'] + $data['insteadOfs'], ); } /** * @return mixed */ + #[ReturnTypeWillChange] public function jsonSerialize() { return [ diff --git a/src/Dependency/ExportedNodeFetcher.php b/src/Dependency/ExportedNodeFetcher.php index 8a4c6beb79..95d82d5057 100644 --- a/src/Dependency/ExportedNodeFetcher.php +++ b/src/Dependency/ExportedNodeFetcher.php @@ -2,27 +2,22 @@ namespace PHPStan\Dependency; +use PhpParser\Node; use PhpParser\NodeTraverser; use PHPStan\Parser\Parser; +use PHPStan\Parser\ParserErrorsException; class ExportedNodeFetcher { - private Parser $parser; - - private ExportedNodeVisitor $visitor; - public function __construct( - Parser $parser, - ExportedNodeVisitor $visitor + private Parser $parser, + private ExportedNodeVisitor $visitor, ) { - $this->parser = $parser; - $this->visitor = $visitor; } /** - * @param string $fileName * @return ExportedNode[] */ public function fetchNodes(string $fileName): array @@ -31,9 +26,9 @@ public function fetchNodes(string $fileName): array $nodeTraverser->addVisitor($this->visitor); try { - /** @var \PhpParser\Node[] $ast */ + /** @var Node[] $ast */ $ast = $this->parser->parseFile($fileName); - } catch (\PHPStan\Parser\ParserErrorsException $e) { + } catch (ParserErrorsException) { return []; } $this->visitor->reset($fileName); diff --git a/src/Dependency/ExportedNodeResolver.php b/src/Dependency/ExportedNodeResolver.php index 0b3981c736..0a2dc26633 100644 --- a/src/Dependency/ExportedNodeResolver.php +++ b/src/Dependency/ExportedNodeResolver.php @@ -7,34 +7,34 @@ use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Function_; -use PhpParser\Node\Stmt\Property; use PhpParser\PrettyPrinter\Standard; use PHPStan\Dependency\ExportedNode\ExportedClassConstantNode; +use PHPStan\Dependency\ExportedNode\ExportedClassConstantsNode; use PHPStan\Dependency\ExportedNode\ExportedClassNode; +use PHPStan\Dependency\ExportedNode\ExportedEnumCaseNode; +use PHPStan\Dependency\ExportedNode\ExportedEnumNode; use PHPStan\Dependency\ExportedNode\ExportedFunctionNode; use PHPStan\Dependency\ExportedNode\ExportedInterfaceNode; use PHPStan\Dependency\ExportedNode\ExportedMethodNode; use PHPStan\Dependency\ExportedNode\ExportedParameterNode; use PHPStan\Dependency\ExportedNode\ExportedPhpDocNode; -use PHPStan\Dependency\ExportedNode\ExportedPropertyNode; +use PHPStan\Dependency\ExportedNode\ExportedPropertiesNode; use PHPStan\Dependency\ExportedNode\ExportedTraitNode; use PHPStan\Dependency\ExportedNode\ExportedTraitUseAdaptation; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; +use function array_map; +use function implode; +use function is_string; class ExportedNodeResolver { - private FileTypeMapper $fileTypeMapper; - - private Standard $printer; - - public function __construct(FileTypeMapper $fileTypeMapper, Standard $printer) + public function __construct(private FileTypeMapper $fileTypeMapper, private Standard $printer) { - $this->fileTypeMapper = $fileTypeMapper; - $this->printer = $printer; } - public function resolve(string $fileName, \PhpParser\Node $node): ?ExportedNode + public function resolve(string $fileName, Node $node): ?ExportedNode { if ($node instanceof Class_ && isset($node->namespacedName)) { $docComment = $node->getDocComment(); @@ -67,7 +67,7 @@ public function resolve(string $fileName, \PhpParser\Node $node): ?ExportedNode $fileName, $className, null, - $docComment !== null ? $docComment->getText() : null + $docComment !== null ? $docComment->getText() : null, ), $node->isAbstract(), $node->isFinal(), @@ -80,7 +80,7 @@ public function resolve(string $fileName, \PhpParser\Node $node): ?ExportedNode $adaptation->trait !== null ? $adaptation->trait->toString() : null, $adaptation->method->toString(), $adaptation->newModifier, - $adaptation->newName !== null ? $adaptation->newName->toString() : null + $adaptation->newName !== null ? $adaptation->newName->toString() : null, ); } @@ -88,21 +88,18 @@ public function resolve(string $fileName, \PhpParser\Node $node): ?ExportedNode return ExportedTraitUseAdaptation::createPrecedence( $adaptation->trait !== null ? $adaptation->trait->toString() : null, $adaptation->method->toString(), - array_map(static function (Name $name): string { - return $name->toString(); - }, $adaptation->insteadof) + array_map(static fn (Name $name): string => $name->toString(), $adaptation->insteadof), ); } - throw new \PHPStan\ShouldNotHappenException(); - }, $adaptations) + throw new ShouldNotHappenException(); + }, $adaptations), + $this->exportClassStatements($node->stmts, $fileName, $className), ); } - if ($node instanceof \PhpParser\Node\Stmt\Interface_ && isset($node->namespacedName)) { - $extendsNames = array_map(static function (Name $name): string { - return (string) $name; - }, $node->extends); + if ($node instanceof Node\Stmt\Interface_ && isset($node->namespacedName)) { + $extendsNames = array_map(static fn (Name $name): string => (string) $name, $node->extends); $docComment = $node->getDocComment(); $interfaceName = $node->namespacedName->toString(); @@ -113,93 +110,39 @@ public function resolve(string $fileName, \PhpParser\Node $node): ?ExportedNode $fileName, $interfaceName, null, - $docComment !== null ? $docComment->getText() : null + $docComment !== null ? $docComment->getText() : null, ), - $extendsNames + $extendsNames, + $this->exportClassStatements($node->stmts, $fileName, $interfaceName), ); } - if ($node instanceof Node\Stmt\Trait_ && isset($node->namespacedName)) { - return new ExportedTraitNode($node->namespacedName->toString()); - } - - if ($node instanceof ClassMethod) { - if ($node->isAbstract() || $node->isFinal() || !$node->isPrivate()) { - $methodName = $node->name->toString(); - $docComment = $node->getDocComment(); - $parentNode = $node->getAttribute('parent'); - $continue = ($parentNode instanceof Class_ || $parentNode instanceof Node\Stmt\Interface_) && isset($parentNode->namespacedName); - if (!$continue) { - return null; - } - - return new ExportedMethodNode( - $methodName, - $this->exportPhpDocNode( - $fileName, - $parentNode->namespacedName->toString(), - $methodName, - $docComment !== null ? $docComment->getText() : null - ), - $node->byRef, - $node->isPublic(), - $node->isPrivate(), - $node->isAbstract(), - $node->isFinal(), - $node->isStatic(), - $this->printType($node->returnType), - $this->exportParameterNodes($node->params) - ); - } - } + if ($node instanceof Node\Stmt\Enum_ && $node->namespacedName !== null) { + $implementsNames = array_map(static fn (Name $name): string => (string) $name, $node->implements); + $docComment = $node->getDocComment(); - if ($node instanceof Node\Stmt\PropertyProperty) { - $parentNode = $node->getAttribute('parent'); - if (!$parentNode instanceof Property) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Expected node type %s, %s occurred.', Property::class, is_object($parentNode) ? get_class($parentNode) : gettype($parentNode))); - } - if ($parentNode->isPrivate()) { - return null; + $enumName = $node->namespacedName->toString(); + $scalarType = null; + if ($node->scalarType !== null) { + $scalarType = $node->scalarType->toString(); } - $classNode = $parentNode->getAttribute('parent'); - if (!$classNode instanceof Class_ || !isset($classNode->namespacedName)) { - return null; - } - - $docComment = $parentNode->getDocComment(); - - return new ExportedPropertyNode( - $node->name->toString(), + return new ExportedEnumNode( + $enumName, + $scalarType, $this->exportPhpDocNode( $fileName, - $classNode->namespacedName->toString(), + $enumName, null, - $docComment !== null ? $docComment->getText() : null + $docComment !== null ? $docComment->getText() : null, ), - $this->printType($parentNode->type), - $parentNode->isPublic(), - $parentNode->isPrivate(), - $parentNode->isStatic() + $implementsNames, + $this->exportClassStatements($node->stmts, $fileName, $enumName), ); } - if ($node instanceof Node\Const_) { - $parentNode = $node->getAttribute('parent'); - if (!$parentNode instanceof Node\Stmt\ClassConst) { - return null; - } - - if ($parentNode->isPrivate()) { - return null; - } - - return new ExportedClassConstantNode( - $node->name->toString(), - $this->printer->prettyPrintExpr($node->value), - $parentNode->isPublic(), - $parentNode->isPrivate() - ); + if ($node instanceof Node\Stmt\Trait_ && isset($node->namespacedName)) { + return new ExportedTraitNode($node->namespacedName->toString()); } if ($node instanceof Function_) { @@ -216,11 +159,11 @@ public function resolve(string $fileName, \PhpParser\Node $node): ?ExportedNode $fileName, null, $functionName, - $docComment !== null ? $docComment->getText() : null + $docComment !== null ? $docComment->getText() : null, ), $node->byRef, $this->printType($node->returnType), - $this->exportParameterNodes($node->params) + $this->exportParameterNodes($node->params), ); } @@ -228,8 +171,7 @@ public function resolve(string $fileName, \PhpParser\Node $node): ?ExportedNode } /** - * @param Node\Identifier|Node\Name|Node\NullableType|Node\UnionType|null $type - * @return string|null + * @param Node\Identifier|Node\Name|Node\ComplexType|null $type */ private function printType($type): ?string { @@ -245,14 +187,29 @@ private function printType($type): ?string return implode('|', array_map(function ($innerType): string { $printedType = $this->printType($innerType); if ($printedType === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); + } + + return $printedType; + }, $type->types)); + } + + if ($type instanceof Node\IntersectionType) { + return implode('&', array_map(function ($innerType): string { + $printedType = $this->printType($innerType); + if ($printedType === null) { + throw new ShouldNotHappenException(); } return $printedType; }, $type->types)); } - return $type->toString(); + if ($type instanceof Node\Identifier || $type instanceof Name) { + return $type->toString(); + } + + throw new ShouldNotHappenException(); } /** @@ -264,7 +221,7 @@ private function exportParameterNodes(array $params): array $nodes = []; foreach ($params as $param) { if (!$param->var instanceof Node\Expr\Variable || !is_string($param->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $type = $param->type; if ( @@ -276,7 +233,7 @@ private function exportParameterNodes(array $params): array $innerTypes = $type->types; $innerTypes[] = new Name('null'); $type = new Node\UnionType($innerTypes); - } elseif (!$type instanceof Node\NullableType) { + } elseif ($type instanceof Node\Identifier || $type instanceof Name) { $type = new Node\NullableType($type); } } @@ -285,7 +242,7 @@ private function exportParameterNodes(array $params): array $this->printType($type), $param->byRef, $param->variadic, - $param->default !== null + $param->default !== null, ); } @@ -296,7 +253,7 @@ private function exportPhpDocNode( string $file, ?string $className, ?string $functionName, - ?string $text + ?string $text, ): ?ExportedPhpDocNode { if ($text === null) { @@ -308,7 +265,7 @@ private function exportPhpDocNode( $className, null, $functionName, - $text + $text, ); $nameScope = $resolvedPhpDocBlock->getNullableNameScope(); @@ -319,4 +276,120 @@ private function exportPhpDocNode( return new ExportedPhpDocNode($text, $nameScope->getNamespace(), $nameScope->getUses()); } + /** + * @param Node\Stmt[] $statements + * @return ExportedNode[] + */ + private function exportClassStatements(array $statements, string $fileName, string $namespacedName): array + { + $exportedNodes = []; + foreach ($statements as $statement) { + $exportedNode = $this->exportClassStatement($statement, $fileName, $namespacedName); + if ($exportedNode === null) { + continue; + } + + $exportedNodes[] = $exportedNode; + } + + return $exportedNodes; + } + + private function exportClassStatement(Node\Stmt $node, string $fileName, string $namespacedName): ?ExportedNode + { + if ($node instanceof ClassMethod) { + if ($node->isAbstract() || $node->isFinal() || !$node->isPrivate()) { + $methodName = $node->name->toString(); + $docComment = $node->getDocComment(); + + return new ExportedMethodNode( + $methodName, + $this->exportPhpDocNode( + $fileName, + $namespacedName, + $methodName, + $docComment !== null ? $docComment->getText() : null, + ), + $node->byRef, + $node->isPublic(), + $node->isPrivate(), + $node->isAbstract(), + $node->isFinal(), + $node->isStatic(), + $this->printType($node->returnType), + $this->exportParameterNodes($node->params), + ); + } + } + + if ($node instanceof Node\Stmt\Property) { + if ($node->isPrivate()) { + return null; + } + + $docComment = $node->getDocComment(); + + return new ExportedPropertiesNode( + array_map(static fn (Node\Stmt\PropertyProperty $prop): string => $prop->name->toString(), $node->props), + $this->exportPhpDocNode( + $fileName, + $namespacedName, + null, + $docComment !== null ? $docComment->getText() : null, + ), + $this->printType($node->type), + $node->isPublic(), + $node->isPrivate(), + $node->isStatic(), + $node->isReadonly(), + ); + } + + if ($node instanceof Node\Stmt\ClassConst) { + if ($node->isPrivate()) { + return null; + } + + $docComment = $node->getDocComment(); + + $constants = []; + foreach ($node->consts as $const) { + $constants[] = new ExportedClassConstantNode( + $const->name->toString(), + $this->printer->prettyPrintExpr($const->value), + ); + } + + return new ExportedClassConstantsNode( + $constants, + $node->isPublic(), + $node->isPrivate(), + $node->isFinal(), + $this->exportPhpDocNode( + $fileName, + $namespacedName, + null, + $docComment !== null ? $docComment->getText() : null, + ), + ); + } + + if ($node instanceof Node\Stmt\EnumCase) { + $docComment = $node->getDocComment(); + + return new ExportedEnumCaseNode( + $node->name->toString(), + $node->expr !== null ? $this->printer->prettyPrintExpr($node->expr) : null, + $this->exportPhpDocNode( + $fileName, + $namespacedName, + null, + $docComment !== null ? $docComment->getText() : null, + ), + ); + } + + return null; + } + } diff --git a/src/Dependency/ExportedNodeVisitor.php b/src/Dependency/ExportedNodeVisitor.php index bedb18c037..61579fba4d 100644 --- a/src/Dependency/ExportedNodeVisitor.php +++ b/src/Dependency/ExportedNodeVisitor.php @@ -5,12 +5,11 @@ use PhpParser\Node; use PhpParser\NodeTraverser; use PhpParser\NodeVisitorAbstract; +use PHPStan\ShouldNotHappenException; class ExportedNodeVisitor extends NodeVisitorAbstract { - private ExportedNodeResolver $exportedNodeResolver; - private ?string $fileName = null; /** @var ExportedNode[] */ @@ -19,11 +18,9 @@ class ExportedNodeVisitor extends NodeVisitorAbstract /** * ExportedNodeVisitor constructor. * - * @param ExportedNodeResolver $exportedNodeResolver */ - public function __construct(ExportedNodeResolver $exportedNodeResolver) + public function __construct(private ExportedNodeResolver $exportedNodeResolver) { - $this->exportedNodeResolver = $exportedNodeResolver; } public function reset(string $fileName): void @@ -43,7 +40,7 @@ public function getExportedNodes(): array public function enterNode(Node $node): ?int { if ($this->fileName === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $exportedNode = $this->exportedNodeResolver->resolve($this->fileName, $node); if ($exportedNode !== null) { diff --git a/src/Dependency/NodeDependencies.php b/src/Dependency/NodeDependencies.php index b693e5091c..2b35652747 100644 --- a/src/Dependency/NodeDependencies.php +++ b/src/Dependency/NodeDependencies.php @@ -2,45 +2,26 @@ namespace PHPStan\Dependency; -use IteratorAggregate; use PHPStan\File\FileHelper; -use PHPStan\Reflection\ReflectionWithFilename; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\FunctionReflection; +use function array_values; -/** - * @implements \IteratorAggregate - */ -class NodeDependencies implements IteratorAggregate +class NodeDependencies { - private FileHelper $fileHelper; - - /** @var array */ - private array $reflections; - - private ?ExportedNode $exportedNode; - /** - * @param FileHelper $fileHelper - * @param array $reflections + * @param array $reflections */ public function __construct( - FileHelper $fileHelper, - array $reflections, - ?ExportedNode $exportedNode + private FileHelper $fileHelper, + private array $reflections, + private ?ExportedNode $exportedNode, ) { - $this->fileHelper = $fileHelper; - $this->reflections = $reflections; - $this->exportedNode = $exportedNode; - } - - public function getIterator(): \Traversable - { - return new \ArrayIterator($this->reflections); } /** - * @param string $currentFile * @param array $analysedFiles * @return string[] */ @@ -50,7 +31,7 @@ public function getFileDependencies(string $currentFile, array $analysedFiles): foreach ($this->reflections as $dependencyReflection) { $dependencyFile = $dependencyReflection->getFileName(); - if ($dependencyFile === false) { + if ($dependencyFile === null) { continue; } $dependencyFile = $this->fileHelper->normalizePath($dependencyFile); diff --git a/src/DependencyInjection/ConditionalTagsExtension.php b/src/DependencyInjection/ConditionalTagsExtension.php index 4808fcc85e..bd2e50c706 100644 --- a/src/DependencyInjection/ConditionalTagsExtension.php +++ b/src/DependencyInjection/ConditionalTagsExtension.php @@ -3,13 +3,17 @@ namespace PHPStan\DependencyInjection; use Nette; +use Nette\DI\CompilerExtension; use Nette\Schema\Expect; use PHPStan\Analyser\TypeSpecifierFactory; use PHPStan\Broker\BrokerFactory; use PHPStan\PhpDoc\TypeNodeResolverExtension; use PHPStan\Rules\RegistryFactory; +use PHPStan\ShouldNotHappenException; +use function count; +use function sprintf; -class ConditionalTagsExtension extends \Nette\DI\CompilerExtension +class ConditionalTagsExtension extends CompilerExtension { public function getConfigSchema(): Nette\Schema\Schema @@ -39,7 +43,7 @@ public function beforeCompile(): void foreach ($config as $type => $tags) { $services = $builder->findByType($type); if (count($services) === 0) { - throw new \PHPStan\ShouldNotHappenException(sprintf('No services of type "%s" found.', $type)); + throw new ShouldNotHappenException(sprintf('No services of type "%s" found.', $type)); } foreach ($services as $service) { foreach ($tags as $tag => $parameter) { diff --git a/src/DependencyInjection/Configurator.php b/src/DependencyInjection/Configurator.php index 1da912305f..1d3ba2b654 100644 --- a/src/DependencyInjection/Configurator.php +++ b/src/DependencyInjection/Configurator.php @@ -4,16 +4,15 @@ use Nette\DI\Config\Loader; use Nette\DI\ContainerLoader; +use function array_keys; +use const PHP_RELEASE_VERSION; +use const PHP_VERSION_ID; -class Configurator extends \Nette\Configurator +class Configurator extends \Nette\Bootstrap\Configurator { - private LoaderFactory $loaderFactory; - - public function __construct(LoaderFactory $loaderFactory) + public function __construct(private LoaderFactory $loaderFactory) { - $this->loaderFactory = $loaderFactory; - parent::__construct(); } @@ -39,12 +38,12 @@ public function loadContainer(): string { $loader = new ContainerLoader( $this->getContainerCacheDirectory(), - $this->parameters['debugMode'] + $this->staticParameters['debugMode'], ); return $loader->load( [$this, 'generateContainer'], - [$this->parameters, array_keys($this->dynamicParameters), $this->configs, PHP_VERSION_ID - PHP_RELEASE_VERSION, NeonAdapter::CACHE_KEY] + [$this->staticParameters, array_keys($this->dynamicParameters), $this->configs, PHP_VERSION_ID - PHP_RELEASE_VERSION, NeonAdapter::CACHE_KEY], ); } diff --git a/src/DependencyInjection/Container.php b/src/DependencyInjection/Container.php index 024b6e4cc9..07a7a574e1 100644 --- a/src/DependencyInjection/Container.php +++ b/src/DependencyInjection/Container.php @@ -9,25 +9,25 @@ interface Container public function hasService(string $serviceName): bool; /** - * @param string $serviceName * @return mixed */ public function getService(string $serviceName); /** - * @param string $className + * @phpstan-template T of object + * @phpstan-param class-string $className + * @phpstan-return T * @return mixed */ public function getByType(string $className); /** - * @param string $className + * @param class-string $className * @return string[] */ public function findServiceNamesByType(string $className): array; /** - * @param string $tagName * @return mixed[] */ public function getServicesByTag(string $tagName): array; @@ -40,9 +40,8 @@ public function getParameters(): array; public function hasParameter(string $parameterName): bool; /** - * @param string $parameterName * @return mixed - * @throws \PHPStan\DependencyInjection\ParameterNotFoundException + * @throws ParameterNotFoundException */ public function getParameter(string $parameterName); diff --git a/src/DependencyInjection/ContainerFactory.php b/src/DependencyInjection/ContainerFactory.php index d1fa6f5746..9a3d2829e8 100644 --- a/src/DependencyInjection/ContainerFactory.php +++ b/src/DependencyInjection/ContainerFactory.php @@ -2,23 +2,33 @@ namespace PHPStan\DependencyInjection; +use Nette\DI\Extensions\ExtensionsExtension; use Nette\DI\Extensions\PhpExtension; use Phar; +use PhpParser\Parser; use PHPStan\BetterReflection\BetterReflection; +use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; +use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; use PHPStan\Broker\Broker; use PHPStan\Command\CommandHelper; use PHPStan\File\FileHelper; use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Reflection\ReflectionProviderStaticAccessor; use Symfony\Component\Finder\Finder; +use function dirname; +use function extension_loaded; +use function ini_get; +use function is_dir; use function sys_get_temp_dir; +use function time; +use function unlink; /** @api */ class ContainerFactory { - private string $currentWorkingDirectory; - private FileHelper $fileHelper; private string $rootDirectory; @@ -26,9 +36,8 @@ class ContainerFactory private string $configDirectory; /** @api */ - public function __construct(string $currentWorkingDirectory) + public function __construct(private string $currentWorkingDirectory) { - $this->currentWorkingDirectory = $currentWorkingDirectory; $this->fileHelper = new FileHelper($currentWorkingDirectory); $rootDir = __DIR__ . '/../..'; @@ -44,17 +53,10 @@ public function __construct(string $currentWorkingDirectory) } /** - * @param string $tempDirectory * @param string[] $additionalConfigFiles * @param string[] $analysedPaths * @param string[] $composerAutoloaderProjectPaths * @param string[] $analysedPathsFromConfig - * @param string $usedLevel - * @param string|null $generateBaselineFile - * @param string|null $cliAutoloadFile - * @param string|null $singleReflectionFile - * @param string|null $singleReflectionInsteadOfFile - * @return \PHPStan\DependencyInjection\Container */ public function create( string $tempDirectory, @@ -66,18 +68,18 @@ public function create( ?string $generateBaselineFile = null, ?string $cliAutoloadFile = null, ?string $singleReflectionFile = null, - ?string $singleReflectionInsteadOfFile = null + ?string $singleReflectionInsteadOfFile = null, ): Container { $configurator = new Configurator(new LoaderFactory( $this->fileHelper, $this->rootDirectory, $this->currentWorkingDirectory, - $generateBaselineFile + $generateBaselineFile, )); $configurator->defaultExtensions = [ 'php' => PhpExtension::class, - 'extensions' => \Nette\DI\Extensions\ExtensionsExtension::class, + 'extensions' => ExtensionsExtension::class, ]; $configurator->setDebugMode(true); $configurator->setTempDirectory($tempDirectory); @@ -87,9 +89,7 @@ public function create( 'cliArgumentsVariablesRegistered' => ini_get('register_argc_argv') === '1', 'tmpDir' => $tempDirectory, 'additionalConfigFiles' => $additionalConfigFiles, - 'analysedPaths' => $analysedPaths, 'composerAutoloaderProjectPaths' => $composerAutoloaderProjectPaths, - 'analysedPathsFromConfig' => $analysedPathsFromConfig, 'generateBaselineFile' => $generateBaselineFile, 'usedLevel' => $usedLevel, 'cliAutoloadFile' => $cliAutoloadFile, @@ -98,6 +98,8 @@ public function create( $configurator->addDynamicParameters([ 'singleReflectionFile' => $singleReflectionFile, 'singleReflectionInsteadOfFile' => $singleReflectionInsteadOfFile, + 'analysedPaths' => $analysedPaths, + 'analysedPathsFromConfig' => $analysedPathsFromConfig, ]); $configurator->addConfig($this->configDirectory . '/config.neon'); foreach ($additionalConfigFiles as $additionalConfigFile) { @@ -106,20 +108,27 @@ public function create( $container = $configurator->createContainer(); - BetterReflection::$phpVersion = $container->getByType(PhpVersion::class)->getVersionId(); + /** @var SourceLocator $sourceLocator */ + $sourceLocator = $container->getService('betterReflectionSourceLocator'); + + /** @var Reflector $reflector */ + $reflector = $container->getService('betterReflectionReflector'); + + /** @var Parser $phpParser */ + $phpParser = $container->getService('phpParserDecorator'); BetterReflection::populate( - $container->getService('betterReflectionSourceLocator'), // @phpstan-ignore-line - $container->getService('betterReflectionClassReflector'), // @phpstan-ignore-line - $container->getService('betterReflectionFunctionReflector'), // @phpstan-ignore-line - $container->getService('betterReflectionConstantReflector'), // @phpstan-ignore-line - $container->getService('phpParserDecorator'), // @phpstan-ignore-line - $container->getByType(PhpStormStubsSourceStubber::class) + $container->getByType(PhpVersion::class)->getVersionId(), + $sourceLocator, + $reflector, + $phpParser, + $container->getByType(PhpStormStubsSourceStubber::class), ); /** @var Broker $broker */ $broker = $container->getByType(Broker::class); Broker::registerInstance($broker); + ReflectionProviderStaticAccessor::registerInstance($container->getByType(ReflectionProvider::class)); $container->getService('typeSpecifier'); BleedingEdgeToggle::setBleedingEdge($container->parameters['featureToggles']['bleedingEdge']); @@ -133,13 +142,18 @@ public function clearOldContainers(string $tempDirectory): void $this->fileHelper, $this->rootDirectory, $this->currentWorkingDirectory, - null + null, )); $configurator->setDebugMode(true); $configurator->setTempDirectory($tempDirectory); + $containerDirectory = $configurator->getContainerCacheDirectory(); + if (!is_dir($containerDirectory)) { + return; + } + $finder = new Finder(); - $finder->name('Container_*')->in($configurator->getContainerCacheDirectory()); + $finder->name('Container_*')->in($containerDirectory); $twoDaysAgo = time() - 24 * 60 * 60 * 2; foreach ($finder as $containerFile) { diff --git a/src/DependencyInjection/DerivativeContainerFactory.php b/src/DependencyInjection/DerivativeContainerFactory.php index ada4f5f921..c859340da9 100644 --- a/src/DependencyInjection/DerivativeContainerFactory.php +++ b/src/DependencyInjection/DerivativeContainerFactory.php @@ -2,67 +2,40 @@ namespace PHPStan\DependencyInjection; +use function array_merge; + class DerivativeContainerFactory { - private string $currentWorkingDirectory; - - private string $tempDirectory; - - /** @var string[] */ - private array $additionalConfigFiles; - - /** @var string[] */ - private array $analysedPaths; - - /** @var string[] */ - private array $composerAutoloaderProjectPaths; - - /** @var string[] */ - private array $analysedPathsFromConfig; - - private string $usedLevel; - - private ?string $generateBaselineFile; - /** - * @param string $currentWorkingDirectory - * @param string $tempDirectory * @param string[] $additionalConfigFiles * @param string[] $analysedPaths * @param string[] $composerAutoloaderProjectPaths * @param string[] $analysedPathsFromConfig - * @param string $usedLevel */ public function __construct( - string $currentWorkingDirectory, - string $tempDirectory, - array $additionalConfigFiles, - array $analysedPaths, - array $composerAutoloaderProjectPaths, - array $analysedPathsFromConfig, - string $usedLevel, - ?string $generateBaselineFile + private string $currentWorkingDirectory, + private string $tempDirectory, + private array $additionalConfigFiles, + private array $analysedPaths, + private array $composerAutoloaderProjectPaths, + private array $analysedPathsFromConfig, + private string $usedLevel, + private ?string $generateBaselineFile, + private ?string $cliAutoloadFile, + private ?string $singleReflectionFile, + private ?string $singleReflectionInsteadOfFile, ) { - $this->currentWorkingDirectory = $currentWorkingDirectory; - $this->tempDirectory = $tempDirectory; - $this->additionalConfigFiles = $additionalConfigFiles; - $this->analysedPaths = $analysedPaths; - $this->composerAutoloaderProjectPaths = $composerAutoloaderProjectPaths; - $this->analysedPathsFromConfig = $analysedPathsFromConfig; - $this->usedLevel = $usedLevel; - $this->generateBaselineFile = $generateBaselineFile; } /** * @param string[] $additionalConfigFiles - * @return \PHPStan\DependencyInjection\Container */ public function create(array $additionalConfigFiles): Container { $containerFactory = new ContainerFactory( - $this->currentWorkingDirectory + $this->currentWorkingDirectory, ); return $containerFactory->create( @@ -72,7 +45,10 @@ public function create(array $additionalConfigFiles): Container $this->composerAutoloaderProjectPaths, $this->analysedPathsFromConfig, $this->usedLevel, - $this->generateBaselineFile + $this->generateBaselineFile, + $this->cliAutoloadFile, + $this->singleReflectionFile, + $this->singleReflectionInsteadOfFile, ); } diff --git a/src/DependencyInjection/InvalidIgnoredErrorPatternsException.php b/src/DependencyInjection/InvalidIgnoredErrorPatternsException.php new file mode 100644 index 0000000000..dd9258c64d --- /dev/null +++ b/src/DependencyInjection/InvalidIgnoredErrorPatternsException.php @@ -0,0 +1,27 @@ +errors)); + } + + /** + * @return string[] + */ + public function getErrors(): array + { + return $this->errors; + } + +} diff --git a/src/DependencyInjection/LoaderFactory.php b/src/DependencyInjection/LoaderFactory.php index eeb35a3110..d12900f5f1 100644 --- a/src/DependencyInjection/LoaderFactory.php +++ b/src/DependencyInjection/LoaderFactory.php @@ -8,25 +8,13 @@ class LoaderFactory { - private FileHelper $fileHelper; - - private string $rootDir; - - private string $currentWorkingDirectory; - - private ?string $generateBaselineFile; - public function __construct( - FileHelper $fileHelper, - string $rootDir, - string $currentWorkingDirectory, - ?string $generateBaselineFile + private FileHelper $fileHelper, + private string $rootDir, + private string $currentWorkingDirectory, + private ?string $generateBaselineFile, ) { - $this->fileHelper = $fileHelper; - $this->rootDir = $rootDir; - $this->currentWorkingDirectory = $currentWorkingDirectory; - $this->generateBaselineFile = $generateBaselineFile; } public function createLoader(): Loader diff --git a/src/DependencyInjection/MemoizingContainer.php b/src/DependencyInjection/MemoizingContainer.php index d43641705c..07e92fbac5 100644 --- a/src/DependencyInjection/MemoizingContainer.php +++ b/src/DependencyInjection/MemoizingContainer.php @@ -2,17 +2,16 @@ namespace PHPStan\DependencyInjection; +use function array_key_exists; + class MemoizingContainer implements Container { - private Container $originalContainer; - /** @var array */ private array $servicesByType = []; - public function __construct(Container $originalContainer) + public function __construct(private Container $originalContainer) { - $this->originalContainer = $originalContainer; } public function hasService(string $serviceName): bool @@ -20,19 +19,11 @@ public function hasService(string $serviceName): bool return $this->originalContainer->hasService($serviceName); } - /** - * @param string $serviceName - * @return mixed - */ public function getService(string $serviceName) { return $this->originalContainer->getService($serviceName); } - /** - * @param string $className - * @return mixed - */ public function getByType(string $className) { if (array_key_exists($className, $this->servicesByType)) { @@ -65,11 +56,6 @@ public function hasParameter(string $parameterName): bool return $this->originalContainer->hasParameter($parameterName); } - /** - * @param string $parameterName - * @return mixed - * @throws ParameterNotFoundException - */ public function getParameter(string $parameterName) { return $this->originalContainer->getParameter($parameterName); diff --git a/src/DependencyInjection/NeonAdapter.php b/src/DependencyInjection/NeonAdapter.php index 8b3b7c9025..1f52ff84f5 100644 --- a/src/DependencyInjection/NeonAdapter.php +++ b/src/DependencyInjection/NeonAdapter.php @@ -6,15 +6,29 @@ use Nette\DI\Config\Helpers; use Nette\DI\Definitions\Reference; use Nette\DI\Definitions\Statement; +use Nette\DI\InvalidConfigurationException; use Nette\Neon\Entity; +use Nette\Neon\Exception; use Nette\Neon\Neon; use PHPStan\File\FileHelper; use PHPStan\File\FileReader; +use function array_values; +use function array_walk_recursive; +use function dirname; +use function implode; +use function in_array; +use function is_array; +use function is_int; +use function is_string; +use function ltrim; +use function sprintf; +use function strpos; +use function substr; class NeonAdapter implements Adapter { - public const CACHE_KEY = 'v12-excludePaths-merge'; + public const CACHE_KEY = 'v17-validate-schema'; private const PREVENT_MERGING_SUFFIX = '!'; @@ -22,7 +36,6 @@ class NeonAdapter implements Adapter private array $fileHelpers = []; /** - * @param string $file * @return mixed[] */ public function load(string $file): array @@ -30,8 +43,8 @@ public function load(string $file): array $contents = FileReader::read($file); try { return $this->process((array) Neon::decode($contents), '', $file); - } catch (\Nette\Neon\Exception $e) { - throw new \Nette\Neon\Exception(sprintf('Error while loading %s: %s', $file, $e->getMessage())); + } catch (Exception $e) { + throw new Exception(sprintf('Error while loading %s: %s', $file, $e->getMessage())); } } @@ -45,7 +58,7 @@ public function process(array $arr, string $fileKey, string $file): array foreach ($arr as $key => $val) { if (is_string($key) && substr($key, -1) === self::PREVENT_MERGING_SUFFIX) { if (!is_array($val) && $val !== null) { - throw new \Nette\DI\InvalidConfigurationException(sprintf('Replacing operator is available only for arrays, item \'%s\' is not array.', $key)); + throw new InvalidConfigurationException(sprintf('Replacing operator is available only for arrays, item \'%s\' is not array.', $key)); } $key = substr($key, 0, -1); $val[Helpers::PREVENT_MERGING] = true; @@ -70,7 +83,7 @@ public function process(array $arr, string $fileKey, string $file): array foreach ($this->process($val->attributes, $fileKeyToPass, $file) as $st) { $tmp = new Statement( $tmp === null ? $st->getEntity() : [$tmp, ltrim(implode('::', (array) $st->getEntity()), ':')], - $st->arguments + $st->arguments, ); } $val = $tmp; @@ -88,8 +101,6 @@ public function process(array $arr, string $fileKey, string $file): array } if (in_array($keyToResolve, [ - '[parameters][autoload_files][]', - '[parameters][autoload_directories][]', '[parameters][paths][]', '[parameters][excludes_analyse][]', '[parameters][excludePaths][]', @@ -97,7 +108,6 @@ public function process(array $arr, string $fileKey, string $file): array '[parameters][excludePaths][analyseAndScan][]', '[parameters][ignoreErrors][][paths][]', '[parameters][ignoreErrors][][path]', - '[parameters][bootstrap]', '[parameters][bootstrapFiles][]', '[parameters][scanFiles][]', '[parameters][scanDirectories][]', @@ -106,7 +116,9 @@ public function process(array $arr, string $fileKey, string $file): array '[parameters][benchmarkFile]', '[parameters][stubFiles][]', '[parameters][symfony][console_application_loader]', + '[parameters][symfony][consoleApplicationLoader]', '[parameters][symfony][container_xml_path]', + '[parameters][symfony][containerXmlPath]', '[parameters][doctrine][objectManagerLoader]', ], true) && is_string($val) && strpos($val, '%') === false && strpos($val, '*') !== 0) { $fileHelper = $this->createFileHelperByFile($file); @@ -128,7 +140,6 @@ public function process(array $arr, string $fileKey, string $file): array /** * @param mixed[] $data - * @return string */ public function dump(array $data): string { @@ -140,7 +151,7 @@ static function (&$val): void { } $val = self::statementToEntity($val); - } + }, ); return "# generated by Nette\n\n" . Neon::encode($data, Neon::BLOCK); } @@ -155,7 +166,7 @@ static function (&$val): void { } elseif ($val instanceof Reference) { $val = '@' . $val->getValue(); } - } + }, ); $entity = $val->getEntity(); @@ -168,7 +179,7 @@ static function (&$val): void { [ self::statementToEntity($entity[0]), new Entity('::' . $entity[1], $val->arguments), - ] + ], ); } elseif ($entity[0] instanceof Reference) { $entity = '@' . $entity[0]->getValue() . '::' . $entity[1]; diff --git a/src/DependencyInjection/NeonLoader.php b/src/DependencyInjection/NeonLoader.php index cee5eb5e1e..4f797d1af7 100644 --- a/src/DependencyInjection/NeonLoader.php +++ b/src/DependencyInjection/NeonLoader.php @@ -2,27 +2,20 @@ namespace PHPStan\DependencyInjection; +use Nette\DI\Config\Loader; use PHPStan\File\FileHelper; -class NeonLoader extends \Nette\DI\Config\Loader +class NeonLoader extends Loader { - private FileHelper $fileHelper; - - private ?string $generateBaselineFile; - public function __construct( - FileHelper $fileHelper, - ?string $generateBaselineFile + private FileHelper $fileHelper, + private ?string $generateBaselineFile, ) { - $this->fileHelper = $fileHelper; - $this->generateBaselineFile = $generateBaselineFile; } /** - * @param string $file - * @param bool|null $merge * @return mixed[] */ public function load(string $file, ?bool $merge = true): array diff --git a/src/DependencyInjection/Nette/NetteContainer.php b/src/DependencyInjection/Nette/NetteContainer.php index 5e57025649..9d9466c8d7 100644 --- a/src/DependencyInjection/Nette/NetteContainer.php +++ b/src/DependencyInjection/Nette/NetteContainer.php @@ -3,6 +3,10 @@ namespace PHPStan\DependencyInjection\Nette; use PHPStan\DependencyInjection\Container; +use PHPStan\DependencyInjection\ParameterNotFoundException; +use function array_key_exists; +use function array_keys; +use function array_map; /** * @internal @@ -10,11 +14,8 @@ class NetteContainer implements Container { - private \Nette\DI\Container $container; - - public function __construct(\Nette\DI\Container $container) + public function __construct(private \Nette\DI\Container $container) { - $this->container = $container; } public function hasService(string $serviceName): bool @@ -23,7 +24,6 @@ public function hasService(string $serviceName): bool } /** - * @param string $serviceName * @return mixed */ public function getService(string $serviceName) @@ -32,7 +32,9 @@ public function getService(string $serviceName) } /** - * @param string $className + * @phpstan-template T of object + * @phpstan-param class-string $className + * @phpstan-return T * @return mixed */ public function getByType(string $className) @@ -41,7 +43,7 @@ public function getByType(string $className) } /** - * @param string $className + * @param class-string $className * @return string[] */ public function findServiceNamesByType(string $className): array @@ -50,7 +52,6 @@ public function findServiceNamesByType(string $className): array } /** - * @param string $tagName * @return mixed[] */ public function getServicesByTag(string $tagName): array @@ -72,13 +73,12 @@ public function hasParameter(string $parameterName): bool } /** - * @param string $parameterName * @return mixed */ public function getParameter(string $parameterName) { if (!$this->hasParameter($parameterName)) { - throw new \PHPStan\DependencyInjection\ParameterNotFoundException($parameterName); + throw new ParameterNotFoundException($parameterName); } return $this->container->parameters[$parameterName]; @@ -90,9 +90,7 @@ public function getParameter(string $parameterName) */ private function tagsToServices(array $tags): array { - return array_map(function (string $serviceName) { - return $this->getService($serviceName); - }, array_keys($tags)); + return array_map(fn (string $serviceName) => $this->getService($serviceName), array_keys($tags)); } } diff --git a/src/DependencyInjection/ParameterNotFoundException.php b/src/DependencyInjection/ParameterNotFoundException.php index 393c5cd779..07a92a376f 100644 --- a/src/DependencyInjection/ParameterNotFoundException.php +++ b/src/DependencyInjection/ParameterNotFoundException.php @@ -2,7 +2,10 @@ namespace PHPStan\DependencyInjection; -class ParameterNotFoundException extends \Exception +use Exception; +use function sprintf; + +class ParameterNotFoundException extends Exception { public function __construct(string $parameterName) diff --git a/src/DependencyInjection/ParametersSchemaExtension.php b/src/DependencyInjection/ParametersSchemaExtension.php index ae35d20efa..3daebbbbad 100644 --- a/src/DependencyInjection/ParametersSchemaExtension.php +++ b/src/DependencyInjection/ParametersSchemaExtension.php @@ -2,48 +2,68 @@ namespace PHPStan\DependencyInjection; +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; -class ParametersSchemaExtension extends \Nette\DI\CompilerExtension +class ParametersSchemaExtension extends CompilerExtension { - public function getConfigSchema(): \Nette\Schema\Schema + 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['__parametersSchema'] = new Statement(Schema::class); - $builder = $this->getContainerBuilder(); - $builder->parameters['__parametersSchema'] = $this->processArgument( + $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 - * @return \Nette\Schema\Schema */ private function processSchema(array $statements): Schema { if (count($statements) === 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $parameterSchema = null; foreach ($statements as $statement) { - $processedArguments = array_map(function ($argument) { - return $this->processArgument($argument); - }, $statement->arguments); + $processedArguments = array_map(fn ($argument) => $this->processArgument($argument), $statement->arguments); if ($parameterSchema === null) { - /** @var \Nette\Schema\Elements\Type|\Nette\Schema\Elements\AnyOf|\Nette\Schema\Elements\Structure $parameterSchema */ + /** @var Type|AnyOf|Structure $parameterSchema */ $parameterSchema = Expect::{$statement->getEntity()}(...$processedArguments); } else { $parameterSchema->{$statement->getEntity()}(...$processedArguments); @@ -66,14 +86,14 @@ private function processArgument($argument) $arguments = []; foreach ($argument->arguments as $schemaArgument) { if (!$schemaArgument instanceof Statement) { - throw new \PHPStan\ShouldNotHappenException('schema() should contain another statement().'); + throw new ShouldNotHappenException('schema() should contain another statement().'); } $arguments[] = $schemaArgument; } if (count($arguments) === 0) { - throw new \PHPStan\ShouldNotHappenException('schema() should have at least one argument.'); + throw new ShouldNotHappenException('schema() should have at least one argument.'); } return $this->processSchema($arguments); diff --git a/src/DependencyInjection/Reflection/DirectClassReflectionExtensionRegistryProvider.php b/src/DependencyInjection/Reflection/DirectClassReflectionExtensionRegistryProvider.php index 06133de037..fa557b070a 100644 --- a/src/DependencyInjection/Reflection/DirectClassReflectionExtensionRegistryProvider.php +++ b/src/DependencyInjection/Reflection/DirectClassReflectionExtensionRegistryProvider.php @@ -13,25 +13,17 @@ class DirectClassReflectionExtensionRegistryProvider implements ClassReflectionExtensionRegistryProvider { - /** @var \PHPStan\Reflection\PropertiesClassReflectionExtension[] */ - private array $propertiesClassReflectionExtensions; - - /** @var \PHPStan\Reflection\MethodsClassReflectionExtension[] */ - private array $methodsClassReflectionExtensions; - private Broker $broker; /** - * @param \PHPStan\Reflection\PropertiesClassReflectionExtension[] $propertiesClassReflectionExtensions - * @param \PHPStan\Reflection\MethodsClassReflectionExtension[] $methodsClassReflectionExtensions + * @param PropertiesClassReflectionExtension[] $propertiesClassReflectionExtensions + * @param MethodsClassReflectionExtension[] $methodsClassReflectionExtensions */ public function __construct( - array $propertiesClassReflectionExtensions, - array $methodsClassReflectionExtensions + private array $propertiesClassReflectionExtensions, + private array $methodsClassReflectionExtensions, ) { - $this->propertiesClassReflectionExtensions = $propertiesClassReflectionExtensions; - $this->methodsClassReflectionExtensions = $methodsClassReflectionExtensions; } public function setBroker(Broker $broker): void @@ -54,7 +46,7 @@ public function getRegistry(): ClassReflectionExtensionRegistry return new ClassReflectionExtensionRegistry( $this->broker, $this->propertiesClassReflectionExtensions, - $this->methodsClassReflectionExtensions + $this->methodsClassReflectionExtensions, ); } diff --git a/src/DependencyInjection/Reflection/LazyClassReflectionExtensionRegistryProvider.php b/src/DependencyInjection/Reflection/LazyClassReflectionExtensionRegistryProvider.php index 41c35dacdf..259600a280 100644 --- a/src/DependencyInjection/Reflection/LazyClassReflectionExtensionRegistryProvider.php +++ b/src/DependencyInjection/Reflection/LazyClassReflectionExtensionRegistryProvider.php @@ -4,21 +4,20 @@ use PHPStan\Broker\Broker; use PHPStan\Broker\BrokerFactory; +use PHPStan\DependencyInjection\Container; use PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension; use PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension; use PHPStan\Reflection\ClassReflectionExtensionRegistry; use PHPStan\Reflection\Php\PhpClassReflectionExtension; +use function array_merge; class LazyClassReflectionExtensionRegistryProvider implements ClassReflectionExtensionRegistryProvider { - private \PHPStan\DependencyInjection\Container $container; + private ?ClassReflectionExtensionRegistry $registry = null; - private ?\PHPStan\Reflection\ClassReflectionExtensionRegistry $registry = null; - - public function __construct(\PHPStan\DependencyInjection\Container $container) + public function __construct(private Container $container) { - $this->container = $container; } public function getRegistry(): ClassReflectionExtensionRegistry @@ -31,7 +30,7 @@ public function getRegistry(): ClassReflectionExtensionRegistry $this->registry = new ClassReflectionExtensionRegistry( $this->container->getByType(Broker::class), array_merge([$phpClassReflectionExtension], $this->container->getServicesByTag(BrokerFactory::PROPERTIES_CLASS_REFLECTION_EXTENSION_TAG), [$annotationsPropertiesClassReflectionExtension]), - array_merge([$phpClassReflectionExtension], $this->container->getServicesByTag(BrokerFactory::METHODS_CLASS_REFLECTION_EXTENSION_TAG), [$annotationsMethodsClassReflectionExtension]) + array_merge([$phpClassReflectionExtension], $this->container->getServicesByTag(BrokerFactory::METHODS_CLASS_REFLECTION_EXTENSION_TAG), [$annotationsMethodsClassReflectionExtension]), ); } diff --git a/src/DependencyInjection/RulesExtension.php b/src/DependencyInjection/RulesExtension.php index b72f286754..391fdb9a1d 100644 --- a/src/DependencyInjection/RulesExtension.php +++ b/src/DependencyInjection/RulesExtension.php @@ -2,13 +2,15 @@ namespace PHPStan\DependencyInjection; +use Nette\DI\CompilerExtension; use Nette\Schema\Expect; +use Nette\Schema\Schema; use PHPStan\Rules\RegistryFactory; -class RulesExtension extends \Nette\DI\CompilerExtension +class RulesExtension extends CompilerExtension { - public function getConfigSchema(): \Nette\Schema\Schema + public function getConfigSchema(): Schema { return Expect::listOf('string'); } @@ -22,7 +24,7 @@ public function loadConfiguration(): void foreach ($config as $key => $rule) { $builder->addDefinition($this->prefix((string) $key)) ->setFactory($rule) - ->setAutowired(false) + ->setAutowired($rule) ->addTag(RegistryFactory::RULE_TAG); } } diff --git a/src/DependencyInjection/Type/DirectDynamicReturnTypeExtensionRegistryProvider.php b/src/DependencyInjection/Type/DirectDynamicReturnTypeExtensionRegistryProvider.php deleted file mode 100644 index 3759ec24ed..0000000000 --- a/src/DependencyInjection/Type/DirectDynamicReturnTypeExtensionRegistryProvider.php +++ /dev/null @@ -1,83 +0,0 @@ -dynamicMethodReturnTypeExtensions = $dynamicMethodReturnTypeExtensions; - $this->dynamicStaticMethodReturnTypeExtensions = $dynamicStaticMethodReturnTypeExtensions; - $this->dynamicFunctionReturnTypeExtensions = $dynamicFunctionReturnTypeExtensions; - } - - public function setBroker(Broker $broker): void - { - $this->broker = $broker; - } - - public function setReflectionProvider(ReflectionProvider $reflectionProvider): void - { - $this->reflectionProvider = $reflectionProvider; - } - - public function addDynamicMethodReturnTypeExtension(DynamicMethodReturnTypeExtension $extension): void - { - $this->dynamicMethodReturnTypeExtensions[] = $extension; - } - - public function addDynamicStaticMethodReturnTypeExtension(DynamicStaticMethodReturnTypeExtension $extension): void - { - $this->dynamicStaticMethodReturnTypeExtensions[] = $extension; - } - - public function addDynamicFunctionReturnTypeExtension(DynamicFunctionReturnTypeExtension $extension): void - { - $this->dynamicFunctionReturnTypeExtensions[] = $extension; - } - - public function getRegistry(): DynamicReturnTypeExtensionRegistry - { - return new DynamicReturnTypeExtensionRegistry( - $this->broker, - $this->reflectionProvider, - $this->dynamicMethodReturnTypeExtensions, - $this->dynamicStaticMethodReturnTypeExtensions, - $this->dynamicFunctionReturnTypeExtensions - ); - } - -} diff --git a/src/DependencyInjection/Type/DirectOperatorTypeSpecifyingExtensionRegistryProvider.php b/src/DependencyInjection/Type/DirectOperatorTypeSpecifyingExtensionRegistryProvider.php deleted file mode 100644 index 80f73d7b5a..0000000000 --- a/src/DependencyInjection/Type/DirectOperatorTypeSpecifyingExtensionRegistryProvider.php +++ /dev/null @@ -1,38 +0,0 @@ -extensions = $extensions; - } - - public function setBroker(Broker $broker): void - { - $this->broker = $broker; - } - - public function getRegistry(): OperatorTypeSpecifyingExtensionRegistry - { - return new OperatorTypeSpecifyingExtensionRegistry( - $this->broker, - $this->extensions - ); - } - -} diff --git a/src/DependencyInjection/Type/LazyDynamicReturnTypeExtensionRegistryProvider.php b/src/DependencyInjection/Type/LazyDynamicReturnTypeExtensionRegistryProvider.php index 1e52ed099c..f992ad9ddf 100644 --- a/src/DependencyInjection/Type/LazyDynamicReturnTypeExtensionRegistryProvider.php +++ b/src/DependencyInjection/Type/LazyDynamicReturnTypeExtensionRegistryProvider.php @@ -4,19 +4,17 @@ use PHPStan\Broker\Broker; use PHPStan\Broker\BrokerFactory; +use PHPStan\DependencyInjection\Container; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\DynamicReturnTypeExtensionRegistry; class LazyDynamicReturnTypeExtensionRegistryProvider implements DynamicReturnTypeExtensionRegistryProvider { - private \PHPStan\DependencyInjection\Container $container; + private ?DynamicReturnTypeExtensionRegistry $registry = null; - private ?\PHPStan\Type\DynamicReturnTypeExtensionRegistry $registry = null; - - public function __construct(\PHPStan\DependencyInjection\Container $container) + public function __construct(private Container $container) { - $this->container = $container; } public function getRegistry(): DynamicReturnTypeExtensionRegistry @@ -27,7 +25,7 @@ public function getRegistry(): DynamicReturnTypeExtensionRegistry $this->container->getByType(ReflectionProvider::class), $this->container->getServicesByTag(BrokerFactory::DYNAMIC_METHOD_RETURN_TYPE_EXTENSION_TAG), $this->container->getServicesByTag(BrokerFactory::DYNAMIC_STATIC_METHOD_RETURN_TYPE_EXTENSION_TAG), - $this->container->getServicesByTag(BrokerFactory::DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG) + $this->container->getServicesByTag(BrokerFactory::DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG), ); } diff --git a/src/DependencyInjection/Type/LazyDynamicThrowTypeExtensionProvider.php b/src/DependencyInjection/Type/LazyDynamicThrowTypeExtensionProvider.php index ef7885034c..4696fb8b21 100644 --- a/src/DependencyInjection/Type/LazyDynamicThrowTypeExtensionProvider.php +++ b/src/DependencyInjection/Type/LazyDynamicThrowTypeExtensionProvider.php @@ -11,11 +11,8 @@ class LazyDynamicThrowTypeExtensionProvider implements DynamicThrowTypeExtension public const METHOD_TAG = 'phpstan.dynamicMethodThrowTypeExtension'; public const STATIC_METHOD_TAG = 'phpstan.dynamicStaticMethodThrowTypeExtension'; - private Container $container; - - public function __construct(Container $container) + public function __construct(private Container $container) { - $this->container = $container; } public function getDynamicFunctionThrowTypeExtensions(): array diff --git a/src/DependencyInjection/Type/LazyOperatorTypeSpecifyingExtensionRegistryProvider.php b/src/DependencyInjection/Type/LazyOperatorTypeSpecifyingExtensionRegistryProvider.php index 3f9f0ca376..22e356fe70 100644 --- a/src/DependencyInjection/Type/LazyOperatorTypeSpecifyingExtensionRegistryProvider.php +++ b/src/DependencyInjection/Type/LazyOperatorTypeSpecifyingExtensionRegistryProvider.php @@ -4,18 +4,16 @@ use PHPStan\Broker\Broker; use PHPStan\Broker\BrokerFactory; +use PHPStan\DependencyInjection\Container; use PHPStan\Type\OperatorTypeSpecifyingExtensionRegistry; class LazyOperatorTypeSpecifyingExtensionRegistryProvider implements OperatorTypeSpecifyingExtensionRegistryProvider { - private \PHPStan\DependencyInjection\Container $container; + private ?OperatorTypeSpecifyingExtensionRegistry $registry = null; - private ?\PHPStan\Type\OperatorTypeSpecifyingExtensionRegistry $registry = null; - - public function __construct(\PHPStan\DependencyInjection\Container $container) + public function __construct(private Container $container) { - $this->container = $container; } public function getRegistry(): OperatorTypeSpecifyingExtensionRegistry @@ -23,7 +21,7 @@ public function getRegistry(): OperatorTypeSpecifyingExtensionRegistry if ($this->registry === null) { $this->registry = new OperatorTypeSpecifyingExtensionRegistry( $this->container->getByType(Broker::class), - $this->container->getServicesByTag(BrokerFactory::OPERATOR_TYPE_SPECIFYING_EXTENSION_TAG) + $this->container->getServicesByTag(BrokerFactory::OPERATOR_TYPE_SPECIFYING_EXTENSION_TAG), ); } diff --git a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php new file mode 100644 index 0000000000..260cee8f6b --- /dev/null +++ b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php @@ -0,0 +1,146 @@ +getContainerBuilder(); + if (!$builder->parameters['__validate']) { + return; + } + + $ignoreErrors = $builder->parameters['ignoreErrors']; + if (count($ignoreErrors) === 0) { + return; + } + + /** @throws void */ + $parser = Llk::load(new Read('hoa://Library/Regex/Grammar.pp')); + $reflectionProvider = new DummyReflectionProvider(); + ReflectionProviderStaticAccessor::registerInstance($reflectionProvider); + $ignoredRegexValidator = new IgnoredRegexValidator( + $parser, + new TypeStringResolver( + new Lexer(), + new TypeParser(new ConstExprParser()), + new TypeNodeResolver( + new DirectTypeNodeResolverExtensionRegistryProvider( + new class implements TypeNodeResolverExtensionRegistry { + + public function getExtensions(): array + { + return []; + } + + }, + ), + new DirectReflectionProviderProvider($reflectionProvider), + new DirectTypeAliasResolverProvider(new class implements TypeAliasResolver { + + public function hasTypeAlias(string $aliasName, ?string $classNameScope): bool + { + return false; + } + + public function resolveTypeAlias(string $aliasName, NameScope $nameScope): ?Type + { + return null; + } + + }), + ), + ), + ); + $errors = []; + + foreach ($ignoreErrors as $ignoreError) { + try { + if (is_array($ignoreError)) { + if (isset($ignoreError['count'])) { + continue; // ignoreError coming from baseline will be correct + } + $ignoreMessage = $ignoreError['message']; + } else { + $ignoreMessage = $ignoreError; + } + + Strings::match('', $ignoreMessage); + $validationResult = $ignoredRegexValidator->validate($ignoreMessage); + $ignoredTypes = $validationResult->getIgnoredTypes(); + if (count($ignoredTypes) > 0) { + $errors[] = $this->createIgnoredTypesError($ignoreMessage, $ignoredTypes); + } + + if ($validationResult->hasAnchorsInTheMiddle()) { + $errors[] = $this->createAnchorInTheMiddleError($ignoreMessage); + } + + if ($validationResult->areAllErrorsIgnored()) { + $errors[] = sprintf("Ignored error %s has an unescaped '%s' which leads to ignoring all errors. Use '%s' instead.", $ignoreMessage, $validationResult->getWrongSequence(), $validationResult->getEscapedWrongSequence()); + } + } catch (RegexpException $e) { + $errors[] = $e->getMessage(); + } + } + + if (count($errors) === 0) { + return; + } + + throw new InvalidIgnoredErrorPatternsException($errors); + } + + /** + * @param array $ignoredTypes + */ + private function createIgnoredTypesError(string $regex, array $ignoredTypes): string + { + return sprintf( + "Ignored error %s has an unescaped '|' which leads to ignoring more errors than intended. Use '\\|' instead.\n%s", + $regex, + sprintf( + "It ignores all errors containing the following types:\n%s", + implode("\n", array_map(static fn (string $typeDescription): string => sprintf('* %s', $typeDescription), array_keys($ignoredTypes))), + ), + ); + } + + private function createAnchorInTheMiddleError(string $regex): string + { + return sprintf("Ignored error %s has an unescaped anchor '$' in the middle. This leads to unintended behavior. Use '\\$' instead.", $regex); + } + +} diff --git a/src/File/CouldNotReadFileException.php b/src/File/CouldNotReadFileException.php index 356d461a11..5803fd53df 100644 --- a/src/File/CouldNotReadFileException.php +++ b/src/File/CouldNotReadFileException.php @@ -2,7 +2,10 @@ namespace PHPStan\File; -class CouldNotReadFileException extends \PHPStan\AnalysedCodeException +use PHPStan\AnalysedCodeException; +use function sprintf; + +class CouldNotReadFileException extends AnalysedCodeException { public function __construct(string $fileName) diff --git a/src/File/CouldNotWriteFileException.php b/src/File/CouldNotWriteFileException.php index 624f10e0d1..0fffa54dcf 100644 --- a/src/File/CouldNotWriteFileException.php +++ b/src/File/CouldNotWriteFileException.php @@ -2,7 +2,10 @@ namespace PHPStan\File; -class CouldNotWriteFileException extends \PHPStan\AnalysedCodeException +use PHPStan\AnalysedCodeException; +use function sprintf; + +class CouldNotWriteFileException extends AnalysedCodeException { public function __construct(string $fileName, string $error) diff --git a/src/File/FileExcluder.php b/src/File/FileExcluder.php index 4b60c7d5c4..3c5cf85f3b 100644 --- a/src/File/FileExcluder.php +++ b/src/File/FileExcluder.php @@ -2,6 +2,16 @@ namespace PHPStan\File; +use function array_merge; +use function fnmatch; +use function in_array; +use function preg_match; +use function strlen; +use function strpos; +use const DIRECTORY_SEPARATOR; +use const FNM_CASEFOLD; +use const FNM_NOESCAPE; + class FileExcluder { @@ -10,20 +20,27 @@ class FileExcluder * * @var string[] */ - private array $analyseExcludes; + private array $literalAnalyseExcludes = []; + + /** + * fnmatch() patterns to use for excluding files and directories from analysing + * @var string[] + */ + private array $fnmatchAnalyseExcludes = []; + + private int $fnmatchFlags; /** - * @param FileHelper $fileHelper * @param string[] $analyseExcludes * @param string[] $stubFiles */ public function __construct( FileHelper $fileHelper, array $analyseExcludes, - array $stubFiles + array $stubFiles, ) { - $this->analyseExcludes = array_map(function (string $exclude) use ($fileHelper): string { + foreach (array_merge($analyseExcludes, $stubFiles) as $exclude) { $len = strlen($exclude); $trailingDirSeparator = ($len > 0 && in_array($exclude[$len - 1], ['\\', '/'], true)); @@ -34,28 +51,29 @@ public function __construct( } if ($this->isFnmatchPattern($normalized)) { - return $normalized; + $this->fnmatchAnalyseExcludes[] = $normalized; + } else { + $this->literalAnalyseExcludes[] = $fileHelper->absolutizePath($normalized); } + } - return $fileHelper->absolutizePath($normalized); - }, array_merge($analyseExcludes, $stubFiles)); + $isWindows = DIRECTORY_SEPARATOR === '\\'; + if ($isWindows) { + $this->fnmatchFlags = FNM_NOESCAPE | FNM_CASEFOLD; + } else { + $this->fnmatchFlags = 0; + } } public function isExcludedFromAnalysing(string $file): bool { - foreach ($this->analyseExcludes as $exclude) { + foreach ($this->literalAnalyseExcludes as $exclude) { if (strpos($file, $exclude) === 0) { return true; } - - $isWindows = DIRECTORY_SEPARATOR === '\\'; - if ($isWindows) { - $fnmatchFlags = FNM_NOESCAPE | FNM_CASEFOLD; - } else { - $fnmatchFlags = 0; - } - - if ($this->isFnmatchPattern($exclude) && fnmatch($exclude, $file, $fnmatchFlags)) { + } + foreach ($this->fnmatchAnalyseExcludes as $exclude) { + if (fnmatch($exclude, $file, $this->fnmatchFlags)) { return true; } } diff --git a/src/File/FileExcluderFactory.php b/src/File/FileExcluderFactory.php index 53ba207582..26976a3dfb 100644 --- a/src/File/FileExcluderFactory.php +++ b/src/File/FileExcluderFactory.php @@ -2,31 +2,24 @@ namespace PHPStan\File; +use function array_key_exists; +use function array_merge; +use function array_unique; +use function array_values; + class FileExcluderFactory { - private FileExcluderRawFactory $fileExcluderRawFactory; - - /** @var string[] */ - private array $obsoleteExcludesAnalyse; - - /** @var array{analyse?: array, analyseAndScan?: array}|null */ - private ?array $excludePaths; - /** - * @param FileExcluderRawFactory $fileExcluderRawFactory * @param string[] $obsoleteExcludesAnalyse * @param array{analyse?: array, analyseAndScan?: array}|null $excludePaths */ public function __construct( - FileExcluderRawFactory $fileExcluderRawFactory, - array $obsoleteExcludesAnalyse, - ?array $excludePaths + private FileExcluderRawFactory $fileExcluderRawFactory, + private array $obsoleteExcludesAnalyse, + private ?array $excludePaths, ) { - $this->fileExcluderRawFactory = $fileExcluderRawFactory; - $this->obsoleteExcludesAnalyse = $obsoleteExcludesAnalyse; - $this->excludePaths = $excludePaths; } public function createAnalyseFileExcluder(): FileExcluder diff --git a/src/File/FileExcluderRawFactory.php b/src/File/FileExcluderRawFactory.php index b2f7e4ee77..0e3550cb3a 100644 --- a/src/File/FileExcluderRawFactory.php +++ b/src/File/FileExcluderRawFactory.php @@ -7,10 +7,9 @@ interface FileExcluderRawFactory /** * @param string[] $analyseExcludes - * @return FileExcluder */ public function create( - array $analyseExcludes + array $analyseExcludes, ): FileExcluder; } diff --git a/src/File/FileFinder.php b/src/File/FileFinder.php index 61469de531..d549d9c2cc 100644 --- a/src/File/FileFinder.php +++ b/src/File/FileFinder.php @@ -3,46 +3,38 @@ namespace PHPStan\File; use Symfony\Component\Finder\Finder; +use function array_filter; +use function array_values; +use function file_exists; +use function implode; +use function is_file; class FileFinder { - private FileExcluder $fileExcluder; - - private FileHelper $fileHelper; - - /** @var string[] */ - private array $fileExtensions; - /** - * @param FileExcluder $fileExcluder - * @param FileHelper $fileHelper * @param string[] $fileExtensions */ public function __construct( - FileExcluder $fileExcluder, - FileHelper $fileHelper, - array $fileExtensions + private FileExcluder $fileExcluder, + private FileHelper $fileHelper, + private array $fileExtensions, ) { - $this->fileExcluder = $fileExcluder; - $this->fileHelper = $fileHelper; - $this->fileExtensions = $fileExtensions; } /** * @param string[] $paths - * @return FileFinderResult */ public function findFiles(array $paths): FileFinderResult { $onlyFiles = true; $files = []; foreach ($paths as $path) { - if (!file_exists($path)) { - throw new \PHPStan\File\PathNotFoundException($path); - } elseif (is_file($path)) { + if (is_file($path)) { $files[] = $this->fileHelper->normalizePath($path); + } elseif (!file_exists($path)) { + throw new PathNotFoundException($path); } else { $finder = new Finder(); $finder->followLinks(); @@ -53,9 +45,7 @@ public function findFiles(array $paths): FileFinderResult } } - $files = array_values(array_filter($files, function (string $file): bool { - return !$this->fileExcluder->isExcludedFromAnalysing($file); - })); + $files = array_values(array_filter($files, fn (string $file): bool => !$this->fileExcluder->isExcludedFromAnalysing($file))); return new FileFinderResult($files, $onlyFiles); } diff --git a/src/File/FileFinderResult.php b/src/File/FileFinderResult.php index db239b00cd..d88bcbb84c 100644 --- a/src/File/FileFinderResult.php +++ b/src/File/FileFinderResult.php @@ -5,19 +5,11 @@ class FileFinderResult { - /** @var string[] */ - private array $files; - - private bool $onlyFiles; - /** * @param string[] $files - * @param bool $onlyFiles */ - public function __construct(array $files, bool $onlyFiles) + public function __construct(private array $files, private bool $onlyFiles) { - $this->files = $files; - $this->onlyFiles = $onlyFiles; } /** diff --git a/src/File/FileHelper.php b/src/File/FileHelper.php index b351f1e11f..06bfcdcf11 100644 --- a/src/File/FileHelper.php +++ b/src/File/FileHelper.php @@ -3,6 +3,17 @@ namespace PHPStan\File; use Nette\Utils\Strings; +use function array_pop; +use function explode; +use function implode; +use function ltrim; +use function rtrim; +use function str_replace; +use function str_starts_with; +use function strpos; +use function substr; +use function trim; +use const DIRECTORY_SEPARATOR; class FileHelper { @@ -19,6 +30,7 @@ public function getWorkingDirectory(): string return $this->workingDirectory; } + /** @api */ public function absolutizePath(string $path): string { if (DIRECTORY_SEPARATOR === '/') { @@ -30,16 +42,23 @@ public function absolutizePath(string $path): string return $path; } } - if (\Nette\Utils\Strings::startsWith($path, 'phar://')) { + if (str_starts_with($path, 'phar://')) { return $path; } return rtrim($this->getWorkingDirectory(), '/\\') . DIRECTORY_SEPARATOR . ltrim($path, '/\\'); } + /** @api */ public function normalizePath(string $originalPath, string $directorySeparator = DIRECTORY_SEPARATOR): string { - $matches = \Nette\Utils\Strings::match($originalPath, '~^([a-z]+)\\:\\/\\/(.+)~'); + $isLocalPath = $originalPath !== '' && $originalPath[0] === '/'; + + $matches = null; + if (!$isLocalPath) { + $matches = Strings::match($originalPath, '~^([a-z]+)\\:\\/\\/(.+)~'); + } + if ($matches !== null) { [, $scheme, $path] = $matches; } else { @@ -47,8 +66,7 @@ public function normalizePath(string $originalPath, string $directorySeparator = $path = $originalPath; } - $path = str_replace('\\', '/', $path); - $path = Strings::replace($path, '~/{2,}~', '/'); + $path = str_replace(['\\', '//', '///', '////'], '/', $path); $pathRoot = strpos($path, '/') === 0 ? $directorySeparator : ''; $pathParts = explode('/', trim($path, '/')); diff --git a/src/File/FileMonitor.php b/src/File/FileMonitor.php index c25856a140..52d91b0729 100644 --- a/src/File/FileMonitor.php +++ b/src/File/FileMonitor.php @@ -2,23 +2,23 @@ namespace PHPStan\File; +use PHPStan\ShouldNotHappenException; use function array_key_exists; +use function array_keys; +use function count; +use function sha1; class FileMonitor { - /** @var FileFinder */ - private $fileFinder; - /** @var array|null */ - private $fileHashes; + private ?array $fileHashes = null; /** @var array|null */ - private $paths; + private ?array $paths = null; - public function __construct(FileFinder $fileFinder) + public function __construct(private FileFinder $fileFinder) { - $this->fileFinder = $fileFinder; } /** @@ -39,7 +39,7 @@ public function initialize(array $paths): void public function getChanges(): FileMonitorResult { if ($this->fileHashes === null || $this->paths === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $finderResult = $this->fileFinder->findFiles($this->paths); $oldFileHashes = $this->fileHashes; @@ -75,7 +75,7 @@ public function getChanges(): FileMonitorResult $newFiles, $changedFiles, $deletedFiles, - count($fileHashes) + count($fileHashes), ); } diff --git a/src/File/FileMonitorResult.php b/src/File/FileMonitorResult.php index 5e58357941..9da77e0bcc 100644 --- a/src/File/FileMonitorResult.php +++ b/src/File/FileMonitorResult.php @@ -2,38 +2,23 @@ namespace PHPStan\File; +use function count; + class FileMonitorResult { - /** @var string[] */ - private $newFiles; - - /** @var string[] */ - private $changedFiles; - - /** @var string[] */ - private $deletedFiles; - - /** @var int */ - private $totalFilesCount; - /** * @param string[] $newFiles * @param string[] $changedFiles * @param string[] $deletedFiles - * @param int $totalFilesCount */ public function __construct( - array $newFiles, - array $changedFiles, - array $deletedFiles, - int $totalFilesCount + private array $newFiles, + private array $changedFiles, + private array $deletedFiles, + private int $totalFilesCount, ) { - $this->newFiles = $newFiles; - $this->changedFiles = $changedFiles; - $this->deletedFiles = $deletedFiles; - $this->totalFilesCount = $totalFilesCount; } public function hasAnyChanges(): bool diff --git a/src/File/FileReader.php b/src/File/FileReader.php index 5a0c5570fc..5bc5220ae7 100644 --- a/src/File/FileReader.php +++ b/src/File/FileReader.php @@ -3,6 +3,7 @@ namespace PHPStan\File; use function file_get_contents; +use function is_file; class FileReader { @@ -10,11 +11,11 @@ class FileReader public static function read(string $fileName): string { if (!is_file($fileName)) { - throw new \PHPStan\File\CouldNotReadFileException($fileName); + throw new CouldNotReadFileException($fileName); } $contents = @file_get_contents($fileName); if ($contents === false) { - throw new \PHPStan\File\CouldNotReadFileException($fileName); + throw new CouldNotReadFileException($fileName); } return $contents; diff --git a/src/File/FileWriter.php b/src/File/FileWriter.php index 2c92623552..2a40c35bb1 100644 --- a/src/File/FileWriter.php +++ b/src/File/FileWriter.php @@ -2,6 +2,9 @@ namespace PHPStan\File; +use function error_get_last; +use function file_put_contents; + class FileWriter { @@ -11,9 +14,9 @@ public static function write(string $fileName, string $contents): void if ($success === false) { $error = error_get_last(); - throw new \PHPStan\File\CouldNotWriteFileException( + throw new CouldNotWriteFileException( $fileName, - $error !== null ? $error['message'] : 'unknown cause' + $error !== null ? $error['message'] : 'unknown cause', ); } } diff --git a/src/File/FuzzyRelativePathHelper.php b/src/File/FuzzyRelativePathHelper.php index 882cc91c99..47452d60a3 100644 --- a/src/File/FuzzyRelativePathHelper.php +++ b/src/File/FuzzyRelativePathHelper.php @@ -2,29 +2,36 @@ namespace PHPStan\File; +use function count; +use function explode; +use function implode; +use function in_array; +use function ltrim; +use function realpath; +use function str_ends_with; +use function strlen; +use function strpos; +use function substr; +use const DIRECTORY_SEPARATOR; + class FuzzyRelativePathHelper implements RelativePathHelper { - private RelativePathHelper $fallbackRelativePathHelper; - private string $directorySeparator; private ?string $pathToTrim = null; /** - * @param RelativePathHelper $fallbackRelativePathHelper - * @param string $currentWorkingDirectory * @param string[] $analysedPaths - * @param string|null $directorySeparator + * @param non-empty-string|null $directorySeparator */ public function __construct( - RelativePathHelper $fallbackRelativePathHelper, + private RelativePathHelper $fallbackRelativePathHelper, string $currentWorkingDirectory, array $analysedPaths, - ?string $directorySeparator = null + ?string $directorySeparator = null, ) { - $this->fallbackRelativePathHelper = $fallbackRelativePathHelper; if ($directorySeparator === null) { $directorySeparator = DIRECTORY_SEPARATOR; } @@ -64,7 +71,7 @@ public function __construct( $pathArray = explode($directorySeparator, $path); $pathTempParts = []; foreach ($pathArray as $i => $pathPart) { - if (\Nette\Utils\Strings::endsWith($pathPart, '.php')) { + if (str_ends_with($pathPart, '.php')) { continue; } if (!isset($pathToTrimArray[$i])) { diff --git a/src/File/ParentDirectoryRelativePathHelper.php b/src/File/ParentDirectoryRelativePathHelper.php index 306f8f0dbf..218d4597fc 100644 --- a/src/File/ParentDirectoryRelativePathHelper.php +++ b/src/File/ParentDirectoryRelativePathHelper.php @@ -2,17 +2,23 @@ namespace PHPStan\File; +use PHPStan\ShouldNotHappenException; +use function array_fill; +use function array_merge; use function array_slice; +use function count; +use function explode; +use function implode; use function str_replace; +use function strpos; +use function substr; +use function trim; class ParentDirectoryRelativePathHelper implements RelativePathHelper { - private string $parentDirectory; - - public function __construct(string $parentDirectory) + public function __construct(private string $parentDirectory) { - $this->parentDirectory = $parentDirectory; } public function getRelativePath(string $filename): string @@ -21,7 +27,6 @@ public function getRelativePath(string $filename): string } /** - * @param string $filename * @return string[] */ public function getFilenameParts(string $filename): array @@ -55,6 +60,10 @@ public function getFilenameParts(string $filename): array $dotsCount = $parentPartsCount - $i; + if ($dotsCount < 0) { + throw new ShouldNotHappenException(); + } + return array_merge(array_fill(0, $dotsCount, '..'), array_slice($filenameParts, $i)); } diff --git a/src/File/PathNotFoundException.php b/src/File/PathNotFoundException.php index 185bcf459c..fb8f4dd191 100644 --- a/src/File/PathNotFoundException.php +++ b/src/File/PathNotFoundException.php @@ -2,15 +2,15 @@ namespace PHPStan\File; -class PathNotFoundException extends \Exception -{ +use Exception; +use function sprintf; - private string $path; +class PathNotFoundException extends Exception +{ - public function __construct(string $path) + public function __construct(private string $path) { parent::__construct(sprintf('Path %s does not exist', $path)); - $this->path = $path; } public function getPath(): string diff --git a/src/File/SimpleRelativePathHelper.php b/src/File/SimpleRelativePathHelper.php index f71341deeb..14181895ca 100644 --- a/src/File/SimpleRelativePathHelper.php +++ b/src/File/SimpleRelativePathHelper.php @@ -2,23 +2,25 @@ namespace PHPStan\File; +use function str_replace; +use function strlen; +use function strpos; +use function substr; + class SimpleRelativePathHelper implements RelativePathHelper { - private string $currentWorkingDirectory; - - public function __construct(string $currentWorkingDirectory) + public function __construct(private string $currentWorkingDirectory) { - $this->currentWorkingDirectory = $currentWorkingDirectory; } public function getRelativePath(string $filename): string { if ($this->currentWorkingDirectory !== '' && strpos($filename, $this->currentWorkingDirectory) === 0) { - return substr($filename, strlen($this->currentWorkingDirectory) + 1); + return str_replace('\\', '/', substr($filename, strlen($this->currentWorkingDirectory) + 1)); } - return $filename; + return str_replace('\\', '/', $filename); } } diff --git a/src/Internal/BytesHelper.php b/src/Internal/BytesHelper.php index 73561e0de5..48a852c0a3 100644 --- a/src/Internal/BytesHelper.php +++ b/src/Internal/BytesHelper.php @@ -2,6 +2,11 @@ namespace PHPStan\Internal; +use PHPStan\ShouldNotHappenException; +use function abs; +use function end; +use function round; + class BytesHelper { @@ -17,7 +22,7 @@ public static function bytes(int $bytes): string } if (!isset($unit)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return round($bytes, 2) . ' ' . $unit; diff --git a/src/Internal/ContainerDynamicReturnTypeExtension.php b/src/Internal/ContainerDynamicReturnTypeExtension.php index 58703f78c0..afb196dbbd 100644 --- a/src/Internal/ContainerDynamicReturnTypeExtension.php +++ b/src/Internal/ContainerDynamicReturnTypeExtension.php @@ -9,11 +9,14 @@ use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function count; +use function in_array; -class ContainerDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension +class ContainerDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { public function getClass(): string @@ -30,17 +33,17 @@ public function isMethodSupported(MethodReflection $methodReflection): bool public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type { - if (count($methodCall->args) === 0) { + if (count($methodCall->getArgs()) === 0) { return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); } - $argType = $scope->getType($methodCall->args[0]->value); + $argType = $scope->getType($methodCall->getArgs()[0]->value); if (!$argType instanceof ConstantStringType) { return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); } $type = new ObjectType($argType->getValue()); - if ($methodReflection->getName() === 'getByType' && count($methodCall->args) >= 2) { - $argType = $scope->getType($methodCall->args[1]->value); + if ($methodReflection->getName() === 'getByType' && count($methodCall->getArgs()) >= 2) { + $argType = $scope->getType($methodCall->getArgs()[1]->value); if ($argType instanceof ConstantBooleanType && $argType->getValue()) { $type = TypeCombinator::addNull($type); } diff --git a/src/Internal/ScopeIsInClassTypeSpecifyingExtension.php b/src/Internal/ScopeIsInClassTypeSpecifyingExtension.php index e90103ffd6..3e63618ff6 100644 --- a/src/Internal/ScopeIsInClassTypeSpecifyingExtension.php +++ b/src/Internal/ScopeIsInClassTypeSpecifyingExtension.php @@ -18,23 +18,14 @@ class ScopeIsInClassTypeSpecifyingExtension implements MethodTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private string $isInMethodName; - - private string $removeNullMethodName; - - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function __construct( - string $isInMethodName, - string $removeNullMethodName, - ReflectionProvider $reflectionProvider + private string $isInMethodName, + private string $removeNullMethodName, + private ReflectionProvider $reflectionProvider, ) { - $this->isInMethodName = $isInMethodName; - $this->removeNullMethodName = $removeNullMethodName; - $this->reflectionProvider = $reflectionProvider; } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void @@ -50,7 +41,7 @@ public function getClass(): string public function isMethodSupported( MethodReflection $methodReflection, MethodCall $node, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): bool { return $methodReflection->getName() === $this->isInMethodName @@ -61,7 +52,7 @@ public function specifyTypes( MethodReflection $methodReflection, MethodCall $node, Scope $scope, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): SpecifiedTypes { $scopeClass = $this->reflectionProvider->getClass(Scope::class); @@ -72,11 +63,11 @@ public function specifyTypes( return $this->typeSpecifier->create( new MethodCall($node->var, $this->removeNullMethodName), TypeCombinator::removeNull( - ParametersAcceptorSelector::selectSingle($methodVariants)->getReturnType() + ParametersAcceptorSelector::selectSingle($methodVariants)->getReturnType(), ), $context, false, - $scope + $scope, ); } diff --git a/src/Internal/SprintfHelper.php b/src/Internal/SprintfHelper.php new file mode 100644 index 0000000000..30d08500fa --- /dev/null +++ b/src/Internal/SprintfHelper.php @@ -0,0 +1,15 @@ +args) < 2) { + if (count($methodCall->getArgs()) < 2) { return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); } - $getterClosureType = $scope->getType($methodCall->args[1]->value); + $getterClosureType = $scope->getType($methodCall->getArgs()[1]->value); return ParametersAcceptorSelector::selectSingle($getterClosureType->getCallableParametersAcceptors($scope))->getReturnType(); } diff --git a/src/Node/BooleanAndNode.php b/src/Node/BooleanAndNode.php index ee0f25b3b4..c4b750de00 100644 --- a/src/Node/BooleanAndNode.php +++ b/src/Node/BooleanAndNode.php @@ -11,20 +11,9 @@ class BooleanAndNode extends NodeAbstract implements VirtualNode { - /** @var BooleanAnd|LogicalAnd */ - private $originalNode; - - private Scope $rightScope; - - /** - * @param BooleanAnd|LogicalAnd $originalNode - * @param Scope $rightScope - */ - public function __construct($originalNode, Scope $rightScope) + public function __construct(private BooleanAnd|LogicalAnd $originalNode, private Scope $rightScope) { parent::__construct($originalNode->getAttributes()); - $this->originalNode = $originalNode; - $this->rightScope = $rightScope; } /** diff --git a/src/Node/BooleanOrNode.php b/src/Node/BooleanOrNode.php index 4a43adac5d..fb0e04134a 100644 --- a/src/Node/BooleanOrNode.php +++ b/src/Node/BooleanOrNode.php @@ -11,20 +11,9 @@ class BooleanOrNode extends NodeAbstract implements VirtualNode { - /** @var BooleanOr|LogicalOr */ - private $originalNode; - - private Scope $rightScope; - - /** - * @param BooleanOr|LogicalOr $originalNode - * @param Scope $rightScope - */ - public function __construct($originalNode, Scope $rightScope) + public function __construct(private BooleanOr|LogicalOr $originalNode, private Scope $rightScope) { parent::__construct($originalNode->getAttributes()); - $this->originalNode = $originalNode; - $this->rightScope = $rightScope; } /** diff --git a/src/Node/BreaklessWhileLoopNode.php b/src/Node/BreaklessWhileLoopNode.php new file mode 100644 index 0000000000..8a824778bc --- /dev/null +++ b/src/Node/BreaklessWhileLoopNode.php @@ -0,0 +1,47 @@ +getAttributes()); + } + + public function getOriginalNode(): While_ + { + return $this->originalNode; + } + + /** + * @return StatementExitPoint[] + */ + public function getExitPoints(): array + { + return $this->exitPoints; + } + + public function getType(): string + { + return 'PHPStan_Node_BreaklessWhileLoop'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/CatchWithUnthrownExceptionNode.php b/src/Node/CatchWithUnthrownExceptionNode.php index 01ad10629b..007fa83218 100644 --- a/src/Node/CatchWithUnthrownExceptionNode.php +++ b/src/Node/CatchWithUnthrownExceptionNode.php @@ -10,15 +10,9 @@ class CatchWithUnthrownExceptionNode extends NodeAbstract implements VirtualNode { - private Catch_ $originalNode; - - private Type $caughtType; - - public function __construct(Catch_ $originalNode, Type $caughtType) + public function __construct(private Catch_ $originalNode, private Type $caughtType, private Type $originalCaughtType) { parent::__construct($originalNode->getAttributes()); - $this->originalNode = $originalNode; - $this->caughtType = $caughtType; } public function getOriginalNode(): Catch_ @@ -31,6 +25,11 @@ public function getCaughtType(): Type return $this->caughtType; } + public function getOriginalCaughtType(): Type + { + return $this->originalCaughtType; + } + public function getType(): string { return 'PHPStan_Node_CatchWithUnthrownExceptionNode'; diff --git a/src/Node/ClassConstantsNode.php b/src/Node/ClassConstantsNode.php index 3ccb91441b..4b725b6bcd 100644 --- a/src/Node/ClassConstantsNode.php +++ b/src/Node/ClassConstantsNode.php @@ -11,25 +11,13 @@ class ClassConstantsNode extends NodeAbstract implements VirtualNode { - private ClassLike $class; - - /** @var ClassConst[] */ - private array $constants; - - /** @var ClassConstantFetch[] */ - private array $fetches; - /** - * @param ClassLike $class * @param ClassConst[] $constants * @param ClassConstantFetch[] $fetches */ - public function __construct(ClassLike $class, array $constants, array $fetches) + public function __construct(private ClassLike $class, private array $constants, private array $fetches) { parent::__construct($class->getAttributes()); - $this->class = $class; - $this->constants = $constants; - $this->fetches = $fetches; } public function getClass(): ClassLike diff --git a/src/Node/ClassMethodsNode.php b/src/Node/ClassMethodsNode.php index 2f029cea34..545441e9b4 100644 --- a/src/Node/ClassMethodsNode.php +++ b/src/Node/ClassMethodsNode.php @@ -11,25 +11,13 @@ class ClassMethodsNode extends NodeAbstract implements VirtualNode { - private ClassLike $class; - - /** @var ClassMethod[] */ - private array $methods; - - /** @var array */ - private array $methodCalls; - /** - * @param ClassLike $class * @param ClassMethod[] $methods * @param array $methodCalls */ - public function __construct(ClassLike $class, array $methods, array $methodCalls) + public function __construct(private ClassLike $class, private array $methods, private array $methodCalls) { parent::__construct($class->getAttributes()); - $this->class = $class; - $this->methods = $methods; - $this->methodCalls = $methodCalls; } public function getClass(): ClassLike diff --git a/src/Node/ClassPropertiesNode.php b/src/Node/ClassPropertiesNode.php index 3f0dde0cd2..feefd78061 100644 --- a/src/Node/ClassPropertiesNode.php +++ b/src/Node/ClassPropertiesNode.php @@ -2,6 +2,7 @@ namespace PHPStan\Node; +use PhpParser\Node; use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Identifier; @@ -15,37 +16,26 @@ use PHPStan\Node\Property\PropertyWrite; use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Properties\ReadWritePropertiesExtension; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; +use function array_key_exists; +use function array_keys; +use function count; +use function in_array; /** @api */ class ClassPropertiesNode extends NodeAbstract implements VirtualNode { - private ClassLike $class; - - /** @var ClassPropertyNode[] */ - private array $properties; - - /** @var array */ - private array $propertyUsages; - - /** @var array */ - private array $methodCalls; - /** - * @param ClassLike $class * @param ClassPropertyNode[] $properties * @param array $propertyUsages * @param array $methodCalls */ - public function __construct(ClassLike $class, array $properties, array $propertyUsages, array $methodCalls) + public function __construct(private ClassLike $class, private array $properties, private array $propertyUsages, private array $methodCalls) { parent::__construct($class->getAttributes()); - $this->class = $class; - $this->properties = $properties; - $this->propertyUsages = $propertyUsages; - $this->methodCalls = $methodCalls; } public function getClass(): ClassLike @@ -85,19 +75,19 @@ public function getSubNodeNames(): array /** * @param string[] $constructors * @param ReadWritePropertiesExtension[] $extensions - * @return array{array, array} + * @return array{array, array, array} */ public function getUninitializedProperties( Scope $scope, array $constructors, - array $extensions + array $extensions, ): array { if (!$this->getClass() instanceof Class_) { - return [[], []]; + return [[], [], []]; } if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $classReflection = $scope->getClassReflection(); @@ -130,11 +120,13 @@ public function getUninitializedProperties( } if ($constructors === []) { - return [$properties, []]; + return [$properties, [], []]; } $classType = new ObjectType($scope->getClassReflection()->getName()); $methodsCalledFromConstructor = $this->getMethodsCalledFromConstructor($classType, $this->methodCalls, $constructors); $prematureAccess = []; + $additionalAssigns = []; + $originalProperties = $properties; foreach ($this->getPropertyUsages() as $usage) { $fetch = $usage->getFetch(); if (!$fetch instanceof PropertyFetch) { @@ -159,9 +151,6 @@ public function getUninitializedProperties( continue; } $propertyName = $fetch->name->toString(); - if (!array_key_exists($propertyName, $properties)) { - continue; - } $fetchedOnType = $usageScope->getType($fetch->var); if ($classType->isSuperTypeOf($fetchedOnType)->no()) { continue; @@ -171,11 +160,20 @@ public function getUninitializedProperties( } if ($usage instanceof PropertyWrite) { - unset($properties[$propertyName]); + if (array_key_exists($propertyName, $properties)) { + unset($properties[$propertyName]); + } elseif (array_key_exists($propertyName, $originalProperties)) { + $additionalAssigns[] = [ + $propertyName, + $fetch->getLine(), + $originalProperties[$propertyName], + ]; + } } elseif (array_key_exists($propertyName, $properties)) { $prematureAccess[] = [ $propertyName, $fetch->getLine(), + $properties[$propertyName], ]; } } @@ -183,11 +181,11 @@ public function getUninitializedProperties( return [ $properties, $prematureAccess, + $additionalAssigns, ]; } /** - * @param ObjectType $classType * @param MethodCall[] $methodCalls * @param string[] $methods * @return string[] @@ -195,7 +193,7 @@ public function getUninitializedProperties( private function getMethodsCalledFromConstructor( ObjectType $classType, array $methodCalls, - array $methods + array $methods, ): array { $originalCount = count($methods); @@ -208,7 +206,7 @@ private function getMethodsCalledFromConstructor( continue; } $callScope = $methodCall->getScope(); - if ($methodCallNode instanceof \PhpParser\Node\Expr\MethodCall) { + if ($methodCallNode instanceof Node\Expr\MethodCall) { $calledOnType = $callScope->getType($methodCallNode->var); } else { if (!$methodCallNode->class instanceof Name) { diff --git a/src/Node/ClassPropertyNode.php b/src/Node/ClassPropertyNode.php index 36bde885b1..b45d2fd931 100644 --- a/src/Node/ClassPropertyNode.php +++ b/src/Node/ClassPropertyNode.php @@ -6,51 +6,24 @@ use PhpParser\Node\Expr; use PhpParser\Node\Identifier; use PhpParser\Node\Name; -use PhpParser\Node\NullableType; use PhpParser\Node\Stmt\Class_; -use PhpParser\Node\UnionType; use PhpParser\NodeAbstract; /** @api */ class ClassPropertyNode extends NodeAbstract implements VirtualNode { - private string $name; - - private int $flags; - - /** @var Identifier|Name|NullableType|UnionType|null */ - private $type; - - private ?Expr $default; - - private ?string $phpDoc; - - private bool $isPromoted; - - /** - * @param int $flags - * @param Identifier|Name|NullableType|UnionType|null $type - * @param string $name - * @param Expr|null $default - */ public function __construct( - string $name, - int $flags, - $type, - ?Expr $default, - ?string $phpDoc, - bool $isPromoted, - Node $originalNode + private string $name, + private int $flags, + private Identifier|Name|Node\ComplexType|null $type, + private ?Expr $default, + private ?string $phpDoc, + private bool $isPromoted, + Node $originalNode, ) { parent::__construct($originalNode->getAttributes()); - $this->name = $name; - $this->flags = $flags; - $this->type = $type; - $this->default = $default; - $this->isPromoted = $isPromoted; - $this->phpDoc = $phpDoc; } public function getName(): string @@ -99,8 +72,13 @@ public function isStatic(): bool return (bool) ($this->flags & Class_::MODIFIER_STATIC); } + public function isReadOnly(): bool + { + return (bool) ($this->flags & Class_::MODIFIER_READONLY); + } + /** - * @return Identifier|Name|NullableType|UnionType|null + * @return Identifier|Name|Node\ComplexType|null */ public function getNativeType() { diff --git a/src/Node/ClassStatementsGatherer.php b/src/Node/ClassStatementsGatherer.php index 14baad972b..80f5a3e3d6 100644 --- a/src/Node/ClassStatementsGatherer.php +++ b/src/Node/ClassStatementsGatherer.php @@ -2,6 +2,7 @@ namespace PHPStan\Node; +use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\ArrayDimFetch; @@ -15,19 +16,19 @@ use PHPStan\Node\Property\PropertyRead; use PHPStan\Node\Property\PropertyWrite; use PHPStan\Reflection\ClassReflection; +use PHPStan\ShouldNotHappenException; +use function count; class ClassStatementsGatherer { - private ClassReflection $classReflection; - - /** @var callable(\PhpParser\Node $node, Scope $scope): void */ + /** @var callable(Node $node, Scope $scope): void */ private $nodeCallback; /** @var ClassPropertyNode[] */ private array $properties = []; - /** @var \PhpParser\Node\Stmt\ClassMethod[] */ + /** @var Node\Stmt\ClassMethod[] */ private array $methods = []; /** @var \PHPStan\Node\Method\MethodCall[] */ @@ -36,22 +37,20 @@ class ClassStatementsGatherer /** @var array */ private array $propertyUsages = []; - /** @var \PhpParser\Node\Stmt\ClassConst[] */ + /** @var Node\Stmt\ClassConst[] */ private array $constants = []; /** @var ClassConstantFetch[] */ private array $constantFetches = []; /** - * @param ClassReflection $classReflection - * @param callable(\PhpParser\Node $node, Scope $scope): void $nodeCallback + * @param callable(Node $node, Scope $scope): void $nodeCallback */ public function __construct( - ClassReflection $classReflection, - callable $nodeCallback + private ClassReflection $classReflection, + callable $nodeCallback, ) { - $this->classReflection = $classReflection; $this->nodeCallback = $nodeCallback; } @@ -64,7 +63,7 @@ public function getProperties(): array } /** - * @return \PhpParser\Node\Stmt\ClassMethod[] + * @return Node\Stmt\ClassMethod[] */ public function getMethods(): array { @@ -88,7 +87,7 @@ public function getPropertyUsages(): array } /** - * @return \PhpParser\Node\Stmt\ClassConst[] + * @return Node\Stmt\ClassConst[] */ public function getConstants(): array { @@ -103,17 +102,17 @@ public function getConstantFetches(): array return $this->constantFetches; } - public function __invoke(\PhpParser\Node $node, Scope $scope): void + public function __invoke(Node $node, Scope $scope): void { $nodeCallback = $this->nodeCallback; $nodeCallback($node, $scope); $this->gatherNodes($node, $scope); } - private function gatherNodes(\PhpParser\Node $node, Scope $scope): void + private function gatherNodes(Node $node, Scope $scope): void { if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ($scope->getClassReflection()->getName() !== $this->classReflection->getName()) { return; @@ -123,16 +122,16 @@ private function gatherNodes(\PhpParser\Node $node, Scope $scope): void if ($node->isPromoted()) { $this->propertyUsages[] = new PropertyWrite( new PropertyFetch(new Expr\Variable('this'), new Identifier($node->getName())), - $scope + $scope, ); } return; } - if ($node instanceof \PhpParser\Node\Stmt\ClassMethod && !$scope->isInTrait()) { + if ($node instanceof Node\Stmt\ClassMethod && !$scope->isInTrait()) { $this->methods[] = $node; return; } - if ($node instanceof \PhpParser\Node\Stmt\ClassConst) { + if ($node instanceof Node\Stmt\ClassConst) { $this->constants[] = $node; return; } @@ -140,6 +139,10 @@ private function gatherNodes(\PhpParser\Node $node, Scope $scope): void $this->methodCalls[] = new \PHPStan\Node\Method\MethodCall($node, $scope); return; } + if ($node instanceof MethodCallableNode || $node instanceof StaticMethodCallableNode) { + $this->methodCalls[] = new \PHPStan\Node\Method\MethodCall($node->getOriginalNode(), $scope); + return; + } if ($node instanceof Array_ && count($node->items) === 2) { $this->methodCalls[] = new \PHPStan\Node\Method\MethodCall($node, $scope); return; @@ -148,6 +151,10 @@ private function gatherNodes(\PhpParser\Node $node, Scope $scope): void $this->constantFetches[] = new ClassConstantFetch($node, $scope); return; } + if ($node instanceof PropertyAssignNode) { + $this->propertyUsages[] = new PropertyWrite($node->getPropertyFetch(), $scope); + return; + } if (!$node instanceof Expr) { return; } @@ -155,10 +162,25 @@ private function gatherNodes(\PhpParser\Node $node, Scope $scope): void $this->gatherNodes($node->var, $scope); return; } - if ($node instanceof \PhpParser\Node\Scalar\EncapsedStringPart) { + if ($node instanceof Expr\AssignRef) { + if (!$node->expr instanceof PropertyFetch && !$node->expr instanceof StaticPropertyFetch) { + $this->gatherNodes($node->expr, $scope); + return; + } + + $this->propertyUsages[] = new PropertyRead($node->expr, $scope); + $this->propertyUsages[] = new PropertyWrite($node->expr, $scope); + return; + } + if ($node instanceof Node\Scalar\EncapsedStringPart) { return; } + $inAssign = $scope->isInExpressionAssign($node); + if ($inAssign) { + return; + } + while ($node instanceof ArrayDimFetch) { $node = $node->var; } @@ -166,11 +188,7 @@ private function gatherNodes(\PhpParser\Node $node, Scope $scope): void return; } - if ($inAssign) { - $this->propertyUsages[] = new PropertyWrite($node, $scope); - } else { - $this->propertyUsages[] = new PropertyRead($node, $scope); - } + $this->propertyUsages[] = new PropertyRead($node, $scope); } } diff --git a/src/Node/ClosureReturnStatementsNode.php b/src/Node/ClosureReturnStatementsNode.php index 07b0a67710..04c229b438 100644 --- a/src/Node/ClosureReturnStatementsNode.php +++ b/src/Node/ClosureReturnStatementsNode.php @@ -2,6 +2,7 @@ namespace PHPStan\Node; +use PhpParser\Node; use PhpParser\Node\Expr\Closure; use PhpParser\Node\Expr\Yield_; use PhpParser\Node\Expr\YieldFrom; @@ -12,34 +13,21 @@ class ClosureReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode { - private \PhpParser\Node\Expr\Closure $closureExpr; - - /** @var \PHPStan\Node\ReturnStatement[] */ - private array $returnStatements; - - /** @var array */ - private array $yieldStatements; - - private StatementResult $statementResult; + private Node\Expr\Closure $closureExpr; /** - * @param \PhpParser\Node\Expr\Closure $closureExpr - * @param \PHPStan\Node\ReturnStatement[] $returnStatements + * @param ReturnStatement[] $returnStatements * @param array $yieldStatements - * @param \PHPStan\Analyser\StatementResult $statementResult */ public function __construct( Closure $closureExpr, - array $returnStatements, - array $yieldStatements, - StatementResult $statementResult + private array $returnStatements, + private array $yieldStatements, + private StatementResult $statementResult, ) { parent::__construct($closureExpr->getAttributes()); $this->closureExpr = $closureExpr; - $this->returnStatements = $returnStatements; - $this->yieldStatements = $yieldStatements; - $this->statementResult = $statementResult; } public function getClosureExpr(): Closure @@ -48,7 +36,7 @@ public function getClosureExpr(): Closure } /** - * @return \PHPStan\Node\ReturnStatement[] + * @return ReturnStatement[] */ public function getReturnStatements(): array { diff --git a/src/Node/Constant/ClassConstantFetch.php b/src/Node/Constant/ClassConstantFetch.php index 0e51150823..30129007d0 100644 --- a/src/Node/Constant/ClassConstantFetch.php +++ b/src/Node/Constant/ClassConstantFetch.php @@ -9,14 +9,8 @@ class ClassConstantFetch { - private ClassConstFetch $node; - - private Scope $scope; - - public function __construct(ClassConstFetch $node, Scope $scope) + public function __construct(private ClassConstFetch $node, private Scope $scope) { - $this->node = $node; - $this->scope = $scope; } public function getNode(): ClassConstFetch diff --git a/src/Node/DoWhileLoopConditionNode.php b/src/Node/DoWhileLoopConditionNode.php new file mode 100644 index 0000000000..c6cbc86572 --- /dev/null +++ b/src/Node/DoWhileLoopConditionNode.php @@ -0,0 +1,46 @@ +getAttributes()); + } + + public function getCond(): Expr + { + return $this->cond; + } + + /** + * @return StatementExitPoint[] + */ + public function getExitPoints(): array + { + return $this->exitPoints; + } + + public function getType(): string + { + return 'PHPStan_Node_ClosureReturnStatementsNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/ExecutionEndNode.php b/src/Node/ExecutionEndNode.php index 48e6bc8441..225036a5ba 100644 --- a/src/Node/ExecutionEndNode.php +++ b/src/Node/ExecutionEndNode.php @@ -10,22 +10,13 @@ class ExecutionEndNode extends NodeAbstract implements VirtualNode { - private Node $node; - - private StatementResult $statementResult; - - private bool $hasNativeReturnTypehint; - public function __construct( - Node $node, - StatementResult $statementResult, - bool $hasNativeReturnTypehint + private Node $node, + private StatementResult $statementResult, + private bool $hasNativeReturnTypehint, ) { parent::__construct($node->getAttributes()); - $this->node = $node; - $this->statementResult = $statementResult; - $this->hasNativeReturnTypehint = $hasNativeReturnTypehint; } public function getNode(): Node diff --git a/src/Node/Expr/GetIterableValueTypeExpr.php b/src/Node/Expr/GetIterableValueTypeExpr.php new file mode 100644 index 0000000000..54e481b2c9 --- /dev/null +++ b/src/Node/Expr/GetIterableValueTypeExpr.php @@ -0,0 +1,34 @@ +getAttributes()); + } + + public function getExpr(): Expr + { + return $this->expr; + } + + public function getType(): string + { + return 'PHPStan_Node_GetIterableValueTypeExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Expr/GetOffsetValueTypeExpr.php b/src/Node/Expr/GetOffsetValueTypeExpr.php new file mode 100644 index 0000000000..3399e4d226 --- /dev/null +++ b/src/Node/Expr/GetOffsetValueTypeExpr.php @@ -0,0 +1,39 @@ +getAttributes()); + } + + public function getVar(): Expr + { + return $this->var; + } + + public function getDim(): Expr + { + return $this->dim; + } + + public function getType(): string + { + return 'PHPStan_Node_GetOffsetValueTypeExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Expr/OriginalPropertyTypeExpr.php b/src/Node/Expr/OriginalPropertyTypeExpr.php new file mode 100644 index 0000000000..113134a994 --- /dev/null +++ b/src/Node/Expr/OriginalPropertyTypeExpr.php @@ -0,0 +1,34 @@ +getAttributes()); + } + + public function getPropertyFetch(): Expr\PropertyFetch|Expr\StaticPropertyFetch + { + return $this->propertyFetch; + } + + public function getType(): string + { + return 'PHPStan_Node_OriginalPropertyTypeExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Expr/SetOffsetValueTypeExpr.php b/src/Node/Expr/SetOffsetValueTypeExpr.php new file mode 100644 index 0000000000..63a9a1f9cc --- /dev/null +++ b/src/Node/Expr/SetOffsetValueTypeExpr.php @@ -0,0 +1,44 @@ +getAttributes()); + } + + public function getVar(): Expr + { + return $this->var; + } + + public function getDim(): ?Expr + { + return $this->dim; + } + + public function getValue(): Expr + { + return $this->value; + } + + public function getType(): string + { + return 'PHPStan_Node_SetOffsetValueTypeExpr'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/FileNode.php b/src/Node/FileNode.php index 298cecab67..13a6596ced 100644 --- a/src/Node/FileNode.php +++ b/src/Node/FileNode.php @@ -2,27 +2,24 @@ namespace PHPStan\Node; +use PhpParser\Node; use PhpParser\NodeAbstract; /** @api */ class FileNode extends NodeAbstract implements VirtualNode { - /** @var \PhpParser\Node[] */ - private array $nodes; - /** - * @param \PhpParser\Node[] $nodes + * @param Node[] $nodes */ - public function __construct(array $nodes) + public function __construct(private array $nodes) { $firstNode = $nodes[0] ?? null; parent::__construct($firstNode !== null ? $firstNode->getAttributes() : []); - $this->nodes = $nodes; } /** - * @return \PhpParser\Node[] + * @return Node[] */ public function getNodes(): array { diff --git a/src/Node/FinallyExitPointsNode.php b/src/Node/FinallyExitPointsNode.php index 7cdafb1487..a87bdc516c 100644 --- a/src/Node/FinallyExitPointsNode.php +++ b/src/Node/FinallyExitPointsNode.php @@ -9,21 +9,13 @@ class FinallyExitPointsNode extends NodeAbstract implements VirtualNode { - /** @var StatementExitPoint[] */ - private array $finallyExitPoints; - - /** @var StatementExitPoint[] */ - private array $tryCatchExitPoints; - /** * @param StatementExitPoint[] $finallyExitPoints * @param StatementExitPoint[] $tryCatchExitPoints */ - public function __construct(array $finallyExitPoints, array $tryCatchExitPoints) + public function __construct(private array $finallyExitPoints, private array $tryCatchExitPoints) { parent::__construct([]); - $this->finallyExitPoints = $finallyExitPoints; - $this->tryCatchExitPoints = $tryCatchExitPoints; } /** diff --git a/src/Node/FunctionCallableNode.php b/src/Node/FunctionCallableNode.php new file mode 100644 index 0000000000..4b2c5589d9 --- /dev/null +++ b/src/Node/FunctionCallableNode.php @@ -0,0 +1,41 @@ +name; + } + + public function getType(): string + { + return 'PHPStan_Node_FunctionCallableNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/FunctionReturnStatementsNode.php b/src/Node/FunctionReturnStatementsNode.php index 0114cd87f5..459da89468 100644 --- a/src/Node/FunctionReturnStatementsNode.php +++ b/src/Node/FunctionReturnStatementsNode.php @@ -10,32 +10,22 @@ class FunctionReturnStatementsNode extends NodeAbstract implements ReturnStatementsNode { - private Function_ $function; - - /** @var \PHPStan\Node\ReturnStatement[] */ - private array $returnStatements; - - private StatementResult $statementResult; - /** - * @param \PhpParser\Node\Stmt\Function_ $function - * @param \PHPStan\Node\ReturnStatement[] $returnStatements - * @param \PHPStan\Analyser\StatementResult $statementResult + * @param ReturnStatement[] $returnStatements + * @param ExecutionEndNode[] $executionEnds */ public function __construct( - Function_ $function, - array $returnStatements, - StatementResult $statementResult + private Function_ $function, + private array $returnStatements, + private StatementResult $statementResult, + private array $executionEnds, ) { parent::__construct($function->getAttributes()); - $this->function = $function; - $this->returnStatements = $returnStatements; - $this->statementResult = $statementResult; } /** - * @return \PHPStan\Node\ReturnStatement[] + * @return ReturnStatement[] */ public function getReturnStatements(): array { @@ -47,11 +37,24 @@ public function getStatementResult(): StatementResult return $this->statementResult; } + /** + * @return ExecutionEndNode[] + */ + public function getExecutionEnds(): array + { + return $this->executionEnds; + } + public function returnsByRef(): bool { return $this->function->byRef; } + public function hasNativeReturnTypehint(): bool + { + return $this->function->returnType !== null; + } + public function getType(): string { return 'PHPStan_Node_FunctionReturnStatementsNode'; diff --git a/src/Node/InArrowFunctionNode.php b/src/Node/InArrowFunctionNode.php index a5070b522d..dd018f32d7 100644 --- a/src/Node/InArrowFunctionNode.php +++ b/src/Node/InArrowFunctionNode.php @@ -2,6 +2,7 @@ namespace PHPStan\Node; +use PhpParser\Node; use PhpParser\Node\Expr\ArrowFunction; use PhpParser\NodeAbstract; @@ -9,7 +10,7 @@ class InArrowFunctionNode extends NodeAbstract implements VirtualNode { - private \PhpParser\Node\Expr\ArrowFunction $originalNode; + private Node\Expr\ArrowFunction $originalNode; public function __construct(ArrowFunction $originalNode) { @@ -17,7 +18,7 @@ public function __construct(ArrowFunction $originalNode) $this->originalNode = $originalNode; } - public function getOriginalNode(): \PhpParser\Node\Expr\ArrowFunction + public function getOriginalNode(): Node\Expr\ArrowFunction { return $this->originalNode; } diff --git a/src/Node/InClassMethodNode.php b/src/Node/InClassMethodNode.php index 8562b138c5..e6c331c208 100644 --- a/src/Node/InClassMethodNode.php +++ b/src/Node/InClassMethodNode.php @@ -2,19 +2,18 @@ namespace PHPStan\Node; +use PhpParser\Node; + /** @api */ -class InClassMethodNode extends \PhpParser\Node\Stmt implements VirtualNode +class InClassMethodNode extends Node\Stmt implements VirtualNode { - private \PhpParser\Node\Stmt\ClassMethod $originalNode; - - public function __construct(\PhpParser\Node\Stmt\ClassMethod $originalNode) + public function __construct(private Node\Stmt\ClassMethod $originalNode) { parent::__construct($originalNode->getAttributes()); - $this->originalNode = $originalNode; } - public function getOriginalNode(): \PhpParser\Node\Stmt\ClassMethod + public function getOriginalNode(): Node\Stmt\ClassMethod { return $this->originalNode; } diff --git a/src/Node/InClassNode.php b/src/Node/InClassNode.php index cb60bf9086..23310aaec0 100644 --- a/src/Node/InClassNode.php +++ b/src/Node/InClassNode.php @@ -2,22 +2,17 @@ namespace PHPStan\Node; +use PhpParser\Node; use PhpParser\Node\Stmt\ClassLike; use PHPStan\Reflection\ClassReflection; /** @api */ -class InClassNode extends \PhpParser\Node\Stmt implements VirtualNode +class InClassNode extends Node\Stmt implements VirtualNode { - private ClassLike $originalNode; - - private ClassReflection $classReflection; - - public function __construct(ClassLike $originalNode, ClassReflection $classReflection) + public function __construct(private ClassLike $originalNode, private ClassReflection $classReflection) { parent::__construct($originalNode->getAttributes()); - $this->originalNode = $originalNode; - $this->classReflection = $classReflection; } public function getOriginalNode(): ClassLike diff --git a/src/Node/InClosureNode.php b/src/Node/InClosureNode.php index 4860c1cc55..43db41bced 100644 --- a/src/Node/InClosureNode.php +++ b/src/Node/InClosureNode.php @@ -2,6 +2,7 @@ namespace PHPStan\Node; +use PhpParser\Node; use PhpParser\Node\Expr\Closure; use PhpParser\NodeAbstract; @@ -9,7 +10,7 @@ class InClosureNode extends NodeAbstract implements VirtualNode { - private \PhpParser\Node\Expr\Closure $originalNode; + private Node\Expr\Closure $originalNode; public function __construct(Closure $originalNode) { diff --git a/src/Node/InForeachNode.php b/src/Node/InForeachNode.php new file mode 100644 index 0000000000..b39026a9c9 --- /dev/null +++ b/src/Node/InForeachNode.php @@ -0,0 +1,34 @@ +getAttributes()); + } + + public function getOriginalNode(): Foreach_ + { + return $this->originalNode; + } + + public function getType(): string + { + return 'PHPStan_Node_InForeachNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/InFunctionNode.php b/src/Node/InFunctionNode.php index f6457a8a71..fc4649c0bb 100644 --- a/src/Node/InFunctionNode.php +++ b/src/Node/InFunctionNode.php @@ -2,19 +2,18 @@ namespace PHPStan\Node; +use PhpParser\Node; + /** @api */ -class InFunctionNode extends \PhpParser\Node\Stmt implements VirtualNode +class InFunctionNode extends Node\Stmt implements VirtualNode { - private \PhpParser\Node\Stmt\Function_ $originalNode; - - public function __construct(\PhpParser\Node\Stmt\Function_ $originalNode) + public function __construct(private Node\Stmt\Function_ $originalNode) { parent::__construct($originalNode->getAttributes()); - $this->originalNode = $originalNode; } - public function getOriginalNode(): \PhpParser\Node\Stmt\Function_ + public function getOriginalNode(): Node\Stmt\Function_ { return $this->originalNode; } diff --git a/src/Node/InstantiationCallableNode.php b/src/Node/InstantiationCallableNode.php new file mode 100644 index 0000000000..acf9a44bf9 --- /dev/null +++ b/src/Node/InstantiationCallableNode.php @@ -0,0 +1,41 @@ +class; + } + + public function getType(): string + { + return 'PHPStan_Node_InstantiationCallableNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/LiteralArrayItem.php b/src/Node/LiteralArrayItem.php index 3321ca4977..48dd0e3705 100644 --- a/src/Node/LiteralArrayItem.php +++ b/src/Node/LiteralArrayItem.php @@ -9,14 +9,8 @@ class LiteralArrayItem { - private Scope $scope; - - private ?ArrayItem $arrayItem; - - public function __construct(Scope $scope, ?ArrayItem $arrayItem) + public function __construct(private Scope $scope, private ?ArrayItem $arrayItem) { - $this->scope = $scope; - $this->arrayItem = $arrayItem; } public function getScope(): Scope diff --git a/src/Node/LiteralArrayNode.php b/src/Node/LiteralArrayNode.php index a542e55c4b..bcbd3f0a93 100644 --- a/src/Node/LiteralArrayNode.php +++ b/src/Node/LiteralArrayNode.php @@ -9,17 +9,12 @@ class LiteralArrayNode extends NodeAbstract implements VirtualNode { - /** @var LiteralArrayItem[] */ - private array $itemNodes; - /** - * @param Array_ $originalNode * @param LiteralArrayItem[] $itemNodes */ - public function __construct(Array_ $originalNode, array $itemNodes) + public function __construct(Array_ $originalNode, private array $itemNodes) { parent::__construct($originalNode->getAttributes()); - $this->itemNodes = $itemNodes; } /** diff --git a/src/Node/MatchExpressionArm.php b/src/Node/MatchExpressionArm.php index bce5ea1f9c..53832b1bcb 100644 --- a/src/Node/MatchExpressionArm.php +++ b/src/Node/MatchExpressionArm.php @@ -6,19 +6,11 @@ class MatchExpressionArm { - /** @var MatchExpressionArmCondition[] */ - private array $conditions; - - private int $line; - /** * @param MatchExpressionArmCondition[] $conditions - * @param int $line */ - public function __construct(array $conditions, int $line) + public function __construct(private array $conditions, private int $line) { - $this->conditions = $conditions; - $this->line = $line; } /** diff --git a/src/Node/MatchExpressionArmCondition.php b/src/Node/MatchExpressionArmCondition.php index a5f9ec421b..2928175be7 100644 --- a/src/Node/MatchExpressionArmCondition.php +++ b/src/Node/MatchExpressionArmCondition.php @@ -9,17 +9,8 @@ class MatchExpressionArmCondition { - private Expr $condition; - - private Scope $scope; - - private int $line; - - public function __construct(Expr $condition, Scope $scope, int $line) + public function __construct(private Expr $condition, private Scope $scope, private int $line) { - $this->condition = $condition; - $this->scope = $scope; - $this->line = $line; } public function getCondition(): Expr diff --git a/src/Node/MatchExpressionNode.php b/src/Node/MatchExpressionNode.php index 5a2dabdc42..00ec3bcf8c 100644 --- a/src/Node/MatchExpressionNode.php +++ b/src/Node/MatchExpressionNode.php @@ -10,28 +10,17 @@ class MatchExpressionNode extends NodeAbstract implements VirtualNode { - private Expr $condition; - - /** @var MatchExpressionArm[] */ - private array $arms; - - private Scope $endScope; - /** - * @param Expr $condition * @param MatchExpressionArm[] $arms */ public function __construct( - Expr $condition, - array $arms, + private Expr $condition, + private array $arms, Expr\Match_ $originalNode, - Scope $endScope + private Scope $endScope, ) { parent::__construct($originalNode->getAttributes()); - $this->condition = $condition; - $this->arms = $arms; - $this->endScope = $endScope; } public function getCondition(): Expr diff --git a/src/Node/Method/MethodCall.php b/src/Node/Method/MethodCall.php index 2138cb2c9c..19c77316fb 100644 --- a/src/Node/Method/MethodCall.php +++ b/src/Node/Method/MethodCall.php @@ -2,6 +2,7 @@ namespace PHPStan\Node\Method; +use PhpParser\Node; use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; @@ -10,23 +11,15 @@ class MethodCall { - /** @var \PhpParser\Node\Expr\MethodCall|StaticCall|Array_ */ - private $node; - - private Scope $scope; - - /** - * @param \PhpParser\Node\Expr\MethodCall|StaticCall|Array_ $node - * @param Scope $scope - */ - public function __construct($node, Scope $scope) + public function __construct( + private Node\Expr\MethodCall|StaticCall|Array_ $node, + private Scope $scope, + ) { - $this->node = $node; - $this->scope = $scope; } /** - * @return \PhpParser\Node\Expr\MethodCall|StaticCall|Array_ + * @return Node\Expr\MethodCall|StaticCall|Array_ */ public function getNode() { diff --git a/src/Node/MethodCallableNode.php b/src/Node/MethodCallableNode.php new file mode 100644 index 0000000000..aff7b5cefc --- /dev/null +++ b/src/Node/MethodCallableNode.php @@ -0,0 +1,52 @@ +getAttributes()); + } + + public function getVar(): Expr + { + return $this->var; + } + + /** + * @return Expr|Identifier + */ + public function getName() + { + return $this->name; + } + + public function getOriginalNode(): Expr\MethodCall + { + return $this->originalNode; + } + + public function getType(): string + { + return 'PHPStan_Node_MethodCallableNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/MethodReturnStatementsNode.php b/src/Node/MethodReturnStatementsNode.php index 7367f0fc72..ca0e3abb82 100644 --- a/src/Node/MethodReturnStatementsNode.php +++ b/src/Node/MethodReturnStatementsNode.php @@ -12,30 +12,23 @@ class MethodReturnStatementsNode extends NodeAbstract implements ReturnStatement private ClassMethod $classMethod; - /** @var \PHPStan\Node\ReturnStatement[] */ - private array $returnStatements; - - private StatementResult $statementResult; - /** - * @param \PhpParser\Node\Stmt\ClassMethod $method - * @param \PHPStan\Node\ReturnStatement[] $returnStatements - * @param \PHPStan\Analyser\StatementResult $statementResult + * @param ReturnStatement[] $returnStatements + * @param ExecutionEndNode[] $executionEnds */ public function __construct( ClassMethod $method, - array $returnStatements, - StatementResult $statementResult + private array $returnStatements, + private StatementResult $statementResult, + private array $executionEnds, ) { parent::__construct($method->getAttributes()); $this->classMethod = $method; - $this->returnStatements = $returnStatements; - $this->statementResult = $statementResult; } /** - * @return \PHPStan\Node\ReturnStatement[] + * @return ReturnStatement[] */ public function getReturnStatements(): array { @@ -47,11 +40,24 @@ public function getStatementResult(): StatementResult return $this->statementResult; } + /** + * @return ExecutionEndNode[] + */ + public function getExecutionEnds(): array + { + return $this->executionEnds; + } + public function returnsByRef(): bool { return $this->classMethod->byRef; } + public function hasNativeReturnTypehint(): bool + { + return $this->classMethod->returnType !== null; + } + public function getType(): string { return 'PHPStan_Node_FunctionReturnStatementsNode'; diff --git a/src/Node/Property/PropertyRead.php b/src/Node/Property/PropertyRead.php index 713b574e74..27bc3ba5c4 100644 --- a/src/Node/Property/PropertyRead.php +++ b/src/Node/Property/PropertyRead.php @@ -10,21 +10,11 @@ class PropertyRead { - /** @var PropertyFetch|StaticPropertyFetch */ - private $fetch; - - private Scope $scope; - - /** - * PropertyWrite constructor. - * - * @param PropertyFetch|StaticPropertyFetch $fetch - * @param Scope $scope - */ - public function __construct($fetch, Scope $scope) + public function __construct( + private PropertyFetch|StaticPropertyFetch $fetch, + private Scope $scope, + ) { - $this->fetch = $fetch; - $this->scope = $scope; } /** diff --git a/src/Node/Property/PropertyWrite.php b/src/Node/Property/PropertyWrite.php index 5d468baa8a..00970b832d 100644 --- a/src/Node/Property/PropertyWrite.php +++ b/src/Node/Property/PropertyWrite.php @@ -10,21 +10,8 @@ class PropertyWrite { - /** @var PropertyFetch|StaticPropertyFetch */ - private $fetch; - - private Scope $scope; - - /** - * PropertyWrite constructor. - * - * @param PropertyFetch|StaticPropertyFetch $fetch - * @param Scope $scope - */ - public function __construct($fetch, Scope $scope) + public function __construct(private PropertyFetch|StaticPropertyFetch $fetch, private Scope $scope) { - $this->fetch = $fetch; - $this->scope = $scope; } /** diff --git a/src/Node/PropertyAssignNode.php b/src/Node/PropertyAssignNode.php new file mode 100644 index 0000000000..62a5d7ac1e --- /dev/null +++ b/src/Node/PropertyAssignNode.php @@ -0,0 +1,48 @@ +getAttributes()); + } + + public function getPropertyFetch(): Expr\PropertyFetch|Expr\StaticPropertyFetch + { + return $this->propertyFetch; + } + + public function getAssignedExpr(): Expr + { + return $this->assignedExpr; + } + + public function isAssignOp(): bool + { + return $this->assignOp; + } + + public function getType(): string + { + return 'PHPStan_Node_PropertyAssignNodeNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/ReturnStatement.php b/src/Node/ReturnStatement.php index 518efc6165..99767fba7b 100644 --- a/src/Node/ReturnStatement.php +++ b/src/Node/ReturnStatement.php @@ -2,6 +2,7 @@ namespace PHPStan\Node; +use PhpParser\Node; use PhpParser\Node\Stmt\Return_; use PHPStan\Analyser\Scope; @@ -9,13 +10,10 @@ class ReturnStatement { - private Scope $scope; + private Node\Stmt\Return_ $returnNode; - private \PhpParser\Node\Stmt\Return_ $returnNode; - - public function __construct(Scope $scope, Return_ $returnNode) + public function __construct(private Scope $scope, Return_ $returnNode) { - $this->scope = $scope; $this->returnNode = $returnNode; } diff --git a/src/Node/ReturnStatementsNode.php b/src/Node/ReturnStatementsNode.php index c49ebee640..f54506d201 100644 --- a/src/Node/ReturnStatementsNode.php +++ b/src/Node/ReturnStatementsNode.php @@ -9,7 +9,7 @@ interface ReturnStatementsNode extends VirtualNode { /** - * @return \PHPStan\Node\ReturnStatement[] + * @return ReturnStatement[] */ public function getReturnStatements(): array; diff --git a/src/Node/StaticMethodCallableNode.php b/src/Node/StaticMethodCallableNode.php new file mode 100644 index 0000000000..a4c6e675ef --- /dev/null +++ b/src/Node/StaticMethodCallableNode.php @@ -0,0 +1,56 @@ +getAttributes()); + } + + /** + * @return Expr|Name + */ + public function getClass() + { + return $this->class; + } + + /** + * @return Identifier|Expr + */ + public function getName() + { + return $this->name; + } + + public function getOriginalNode(): Expr\StaticCall + { + return $this->originalNode; + } + + public function getType(): string + { + return 'PHPStan_Node_StaticMethodCallableNode'; + } + + /** + * @return string[] + */ + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/UnreachableStatementNode.php b/src/Node/UnreachableStatementNode.php index 90e8258606..fb05c30a50 100644 --- a/src/Node/UnreachableStatementNode.php +++ b/src/Node/UnreachableStatementNode.php @@ -8,12 +8,9 @@ class UnreachableStatementNode extends Stmt implements VirtualNode { - private Stmt $originalStatement; - - public function __construct(Stmt $originalStatement) + public function __construct(private Stmt $originalStatement) { parent::__construct($originalStatement->getAttributes()); - $this->originalStatement = $originalStatement; } public function getOriginalStatement(): Stmt diff --git a/src/NodeVisitor/StatementOrderVisitor.php b/src/NodeVisitor/StatementOrderVisitor.php index 867db98fca..896c230275 100644 --- a/src/NodeVisitor/StatementOrderVisitor.php +++ b/src/NodeVisitor/StatementOrderVisitor.php @@ -4,6 +4,8 @@ use PhpParser\Node; use PhpParser\NodeVisitorAbstract; +use function array_pop; +use function count; class StatementOrderVisitor extends NodeVisitorAbstract { @@ -31,7 +33,6 @@ public function beforeTraverse(array $nodes) } /** - * @param Node $node * @return null */ public function enterNode(Node $node) @@ -67,7 +68,6 @@ public function enterNode(Node $node) } /** - * @param Node $node * @return null */ public function leaveNode(Node $node) diff --git a/src/Parallel/ParallelAnalyser.php b/src/Parallel/ParallelAnalyser.php index c0d4edcad7..3cae4c553b 100644 --- a/src/Parallel/ParallelAnalyser.php +++ b/src/Parallel/ParallelAnalyser.php @@ -2,6 +2,7 @@ namespace PHPStan\Parallel; +use Closure; use Clue\React\NDJson\Decoder; use Clue\React\NDJson\Encoder; use Nette\Utils\Random; @@ -11,48 +12,50 @@ use PHPStan\Process\ProcessHelper; use React\EventLoop\StreamSelectLoop; use React\Socket\ConnectionInterface; +use React\Socket\TcpServer; use Symfony\Component\Console\Input\InputInterface; +use Throwable; +use function array_map; +use function array_pop; +use function array_reverse; +use function count; +use function defined; +use function escapeshellarg; +use function is_string; +use function max; use function parse_url; +use function sprintf; +use const PHP_URL_PORT; class ParallelAnalyser { - private int $internalErrorsCountLimit; + private const DEFAULT_TIMEOUT = 600.0; private float $processTimeout; private ProcessPool $processPool; - private int $decoderBufferSize; - public function __construct( - int $internalErrorsCountLimit, + private int $internalErrorsCountLimit, float $processTimeout, - int $decoderBufferSize + private int $decoderBufferSize, ) { - $this->internalErrorsCountLimit = $internalErrorsCountLimit; - $this->processTimeout = $processTimeout; - $this->decoderBufferSize = $decoderBufferSize; + $this->processTimeout = max($processTimeout, self::DEFAULT_TIMEOUT); } /** - * @param Schedule $schedule - * @param string $mainScript - * @param \Closure(int): void|null $postFileCallback - * @param string|null $projectConfigFile - * @param string|null $tmpFile - * @param string|null $insteadOfFile - * @return AnalyserResult + * @param Closure(int ): void|null $postFileCallback */ public function analyse( Schedule $schedule, string $mainScript, - ?\Closure $postFileCallback, + ?Closure $postFileCallback, ?string $projectConfigFile, ?string $tmpFile, ?string $insteadOfFile, - InputInterface $input + InputInterface $input, ): AnalyserResult { $jobs = array_reverse($schedule->getJobs()); @@ -62,11 +65,14 @@ public function analyse( $errors = []; $internalErrors = []; - $server = new \React\Socket\TcpServer('127.0.0.1:0', $loop); + $server = new TcpServer('127.0.0.1:0', $loop); $this->processPool = new ProcessPool($server); $server->on('connection', function (ConnectionInterface $connection) use (&$jobs): void { - $decoder = new Decoder($connection, true, 512, defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0, $this->decoderBufferSize); - $encoder = new Encoder($connection, defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0); + // phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly + $jsonInvalidUtf8Ignore = defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0; + // phpcs:enable + $decoder = new Decoder($connection, true, 512, $jsonInvalidUtf8Ignore, $this->decoderBufferSize); + $encoder = new Encoder($connection, $jsonInvalidUtf8Ignore); $decoder->on('data', function (array $data) use (&$jobs, $decoder, $encoder): void { if ($data['action'] !== 'hello') { return; @@ -94,7 +100,7 @@ public function analyse( $reachedInternalErrorsCountLimit = false; - $handleError = function (\Throwable $error) use (&$internalErrors, &$internalErrorsCount, &$reachedInternalErrorsCountLimit): void { + $handleError = function (Throwable $error) use (&$internalErrors, &$internalErrorsCount, &$reachedInternalErrorsCountLimit): void { $internalErrors[] = sprintf('Internal error: ' . $error->getMessage()); $internalErrorsCount++; $reachedInternalErrorsCountLimit = true; @@ -128,7 +134,7 @@ public function analyse( 'worker', $projectConfigFile, $commandOptions, - $input + $input, ), $loop, $this->processTimeout); $process->start(function (array $json) use ($process, &$internalErrors, &$errors, &$dependencies, &$exportedNodes, &$jobs, $postFileCallback, &$internalErrorsCount, &$reachedInternalErrorsCountLimit, $processIdentifier): void { foreach ($json['errors'] as $jsonError) { @@ -207,7 +213,7 @@ public function analyse( $internalErrors, $internalErrorsCount === 0 ? $dependencies : null, $exportedNodes, - $reachedInternalErrorsCountLimit + $reachedInternalErrorsCountLimit, ); } diff --git a/src/Parallel/Process.php b/src/Parallel/Process.php index 8510b92bc8..0cb84ee154 100644 --- a/src/Parallel/Process.php +++ b/src/Parallel/Process.php @@ -2,22 +2,25 @@ namespace PHPStan\Parallel; +use Exception; +use PHPStan\ShouldNotHappenException; use React\EventLoop\LoopInterface; use React\EventLoop\TimerInterface; use React\Stream\ReadableStreamInterface; use React\Stream\WritableStreamInterface; +use Throwable; +use function fclose; +use function is_string; +use function rewind; +use function sprintf; +use function stream_get_contents; +use function tmpfile; class Process { - private string $command; - public \React\ChildProcess\Process $process; - private LoopInterface $loop; - - private float $timeoutSeconds; - private WritableStreamInterface $in; /** @var resource */ @@ -29,36 +32,33 @@ class Process /** @var callable(mixed[] $json) : void */ private $onData; - /** @var callable(\Throwable $exception) : void */ + /** @var callable(Throwable $exception): void */ private $onError; private ?TimerInterface $timer = null; public function __construct( - string $command, - LoopInterface $loop, - float $timeoutSeconds + private string $command, + private LoopInterface $loop, + private float $timeoutSeconds, ) { - $this->command = $command; - $this->loop = $loop; - $this->timeoutSeconds = $timeoutSeconds; } /** * @param callable(mixed[] $json) : void $onData - * @param callable(\Throwable $exception) : void $onError + * @param callable(Throwable $exception): void $onError * @param callable(?int $exitCode, string $output) : void $onExit */ public function start(callable $onData, callable $onError, callable $onExit): void { $tmpStdOut = tmpfile(); if ($tmpStdOut === false) { - throw new \PHPStan\ShouldNotHappenException('Failed creating temp file for stdout.'); + throw new ShouldNotHappenException('Failed creating temp file for stdout.'); } $tmpStdErr = tmpfile(); if ($tmpStdErr === false) { - throw new \PHPStan\ShouldNotHappenException('Failed creating temp file for stderr.'); + throw new ShouldNotHappenException('Failed creating temp file for stderr.'); } $this->stdOut = $tmpStdOut; $this->stdErr = $tmpStdErr; @@ -109,7 +109,7 @@ public function request(array $data): void $this->in->write($data); $this->timer = $this->loop->addTimer($this->timeoutSeconds, function (): void { $onError = $this->onError; - $onError(new \Exception(sprintf('Child process timed out after %.1f seconds. Try making it longer with parallel.processTimeout setting.', $this->timeoutSeconds))); + $onError(new Exception(sprintf('Child process timed out after %.1f seconds. Try making it longer with parallel.processTimeout setting.', $this->timeoutSeconds))); }); } @@ -130,6 +130,7 @@ public function quit(): void public function bindConnection(ReadableStreamInterface $out, WritableStreamInterface $in): void { $out->on('data', function (array $json): void { + $this->cancelTimer(); if ($json['action'] !== 'result') { return; } @@ -138,11 +139,11 @@ public function bindConnection(ReadableStreamInterface $out, WritableStreamInter $onData($json['result']); }); $this->in = $in; - $out->on('error', function (\Throwable $error): void { + $out->on('error', function (Throwable $error): void { $onError = $this->onError; $onError($error); }); - $in->on('error', function (\Throwable $error): void { + $in->on('error', function (Throwable $error): void { $onError = $this->onError; $onError($error); }); diff --git a/src/Parallel/ProcessPool.php b/src/Parallel/ProcessPool.php index a411712cef..bebb8f8293 100644 --- a/src/Parallel/ProcessPool.php +++ b/src/Parallel/ProcessPool.php @@ -2,26 +2,27 @@ namespace PHPStan\Parallel; +use PHPStan\ShouldNotHappenException; use React\Socket\TcpServer; use function array_key_exists; +use function array_keys; +use function count; +use function sprintf; class ProcessPool { - private TcpServer $server; - /** @var array */ private array $processes = []; - public function __construct(TcpServer $server) + public function __construct(private TcpServer $server) { - $this->server = $server; } public function getProcess(string $identifier): Process { if (!array_key_exists($identifier, $this->processes)) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Process %s not found.', $identifier)); + throw new ShouldNotHappenException(sprintf('Process %s not found.', $identifier)); } return $this->processes[$identifier]; diff --git a/src/Parallel/Schedule.php b/src/Parallel/Schedule.php index fef0e12903..42935bd6c5 100644 --- a/src/Parallel/Schedule.php +++ b/src/Parallel/Schedule.php @@ -5,19 +5,11 @@ class Schedule { - private int $numberOfProcesses; - - /** @var array> */ - private array $jobs; - /** - * @param int $numberOfProcesses * @param array> $jobs */ - public function __construct(int $numberOfProcesses, array $jobs) + public function __construct(private int $numberOfProcesses, private array $jobs) { - $this->numberOfProcesses = $numberOfProcesses; - $this->jobs = $jobs; } public function getNumberOfProcesses(): int diff --git a/src/Parallel/Scheduler.php b/src/Parallel/Scheduler.php index 1a3e6126d5..b6cc240ae8 100644 --- a/src/Parallel/Scheduler.php +++ b/src/Parallel/Scheduler.php @@ -2,40 +2,38 @@ namespace PHPStan\Parallel; +use function array_chunk; +use function count; +use function floor; +use function max; +use function min; + class Scheduler { - private int $jobSize; - - private int $maximumNumberOfProcesses; - - private int $minimumNumberOfJobsPerProcess; - + /** + * @param positive-int $jobSize + */ public function __construct( - int $jobSize, - int $maximumNumberOfProcesses, - int $minimumNumberOfJobsPerProcess + private int $jobSize, + private int $maximumNumberOfProcesses, + private int $minimumNumberOfJobsPerProcess, ) { - $this->jobSize = $jobSize; - $this->maximumNumberOfProcesses = $maximumNumberOfProcesses; - $this->minimumNumberOfJobsPerProcess = $minimumNumberOfJobsPerProcess; } /** - * @param int $cpuCores * @param array $files - * @return Schedule */ public function scheduleWork( int $cpuCores, - array $files + array $files, ): Schedule { $jobs = array_chunk($files, $this->jobSize); $numberOfProcesses = min( max((int) floor(count($jobs) / $this->minimumNumberOfJobsPerProcess), 1), - $cpuCores + $cpuCores, ); return new Schedule(min($numberOfProcesses, $this->maximumNumberOfProcesses), $jobs); diff --git a/src/Parser/CachedParser.php b/src/Parser/CachedParser.php index f78bfc60f1..225375678c 100644 --- a/src/Parser/CachedParser.php +++ b/src/Parser/CachedParser.php @@ -2,35 +2,31 @@ namespace PHPStan\Parser; +use PhpParser\Node; use PHPStan\File\FileReader; +use function array_slice; class CachedParser implements Parser { - private \PHPStan\Parser\Parser $originalParser; - - /** @var array*/ + /** @var array*/ private array $cachedNodesByString = []; private int $cachedNodesByStringCount = 0; - private int $cachedNodesByStringCountMax; - /** @var array */ private array $parsedByString = []; public function __construct( - Parser $originalParser, - int $cachedNodesByStringCountMax + private Parser $originalParser, + private int $cachedNodesByStringCountMax, ) { - $this->originalParser = $originalParser; - $this->cachedNodesByStringCountMax = $cachedNodesByStringCountMax; } /** * @param string $file path to a file to parse - * @return \PhpParser\Node\Stmt[] + * @return Node\Stmt[] */ public function parseFile(string $file): array { @@ -39,7 +35,7 @@ public function parseFile(string $file): array $this->cachedNodesByString, 1, null, - true + true, ); --$this->cachedNodesByStringCount; @@ -56,8 +52,7 @@ public function parseFile(string $file): array } /** - * @param string $sourceCode - * @return \PhpParser\Node\Stmt[] + * @return Node\Stmt[] */ public function parseString(string $sourceCode): array { @@ -66,7 +61,7 @@ public function parseString(string $sourceCode): array $this->cachedNodesByString, 1, null, - true + true, ); --$this->cachedNodesByStringCount; @@ -92,7 +87,7 @@ public function getCachedNodesByStringCountMax(): int } /** - * @return array + * @return array */ public function getCachedNodesByString(): array { diff --git a/src/Parser/CleaningParser.php b/src/Parser/CleaningParser.php new file mode 100644 index 0000000000..98db0e64ef --- /dev/null +++ b/src/Parser/CleaningParser.php @@ -0,0 +1,41 @@ +traverser = new NodeTraverser(); + $this->traverser->addVisitor(new CleaningVisitor()); + $this->traverser->addVisitor(new RemoveUnusedCodeByPhpVersionIdVisitor($phpVersion->getVersionString())); + } + + public function parseFile(string $file): array + { + return $this->clean($this->wrappedParser->parseFile($file)); + } + + public function parseString(string $sourceCode): array + { + return $this->clean($this->wrappedParser->parseString($sourceCode)); + } + + /** + * @param Stmt[] $ast + * @return Stmt[] + */ + private function clean(array $ast): array + { + /** @var Stmt[] */ + return $this->traverser->traverse($ast); + } + +} diff --git a/src/Parser/CleaningVisitor.php b/src/Parser/CleaningVisitor.php new file mode 100644 index 0000000000..b5d406cf17 --- /dev/null +++ b/src/Parser/CleaningVisitor.php @@ -0,0 +1,73 @@ +nodeFinder = new NodeFinder(); + } + + public function enterNode(Node $node): ?Node + { + if ($node instanceof Node\Stmt\Function_) { + $node->stmts = $this->keepVariadicsAndYields($node->stmts); + return $node; + } + + if ($node instanceof Node\Stmt\ClassMethod && $node->stmts !== null) { + $node->stmts = $this->keepVariadicsAndYields($node->stmts); + return $node; + } + + if ($node instanceof Node\Expr\Closure) { + $node->stmts = $this->keepVariadicsAndYields($node->stmts); + return $node; + } + + return null; + } + + /** + * @param Node\Stmt[] $stmts + * @return Node\Stmt[] + */ + private function keepVariadicsAndYields(array $stmts): array + { + $results = $this->nodeFinder->find($stmts, static function (Node $node): bool { + if ($node instanceof Node\Expr\YieldFrom || $node instanceof Node\Expr\Yield_) { + return true; + } + if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name) { + return in_array($node->name->toLowerString(), ParametersAcceptor::VARIADIC_FUNCTIONS, true); + } + + return false; + }); + $newStmts = []; + foreach ($results as $result) { + if ($result instanceof Node\Expr\Yield_ || $result instanceof Node\Expr\YieldFrom) { + $newStmts[] = new Node\Stmt\Expression($result); + continue; + } + if (!$result instanceof Node\Expr\FuncCall) { + continue; + } + + $newStmts[] = new Node\Stmt\Expression(new Node\Expr\FuncCall(new Node\Name\FullyQualified('func_get_args'))); + } + + return $newStmts; + } + +} diff --git a/src/Parser/FunctionCallStatementFinder.php b/src/Parser/FunctionCallStatementFinder.php index 7336d986d8..4f1b190d35 100644 --- a/src/Parser/FunctionCallStatementFinder.php +++ b/src/Parser/FunctionCallStatementFinder.php @@ -2,8 +2,11 @@ namespace PHPStan\Parser; +use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Name; +use function in_array; +use function is_array; class FunctionCallStatementFinder { @@ -11,9 +14,8 @@ class FunctionCallStatementFinder /** * @param string[] $functionNames * @param mixed $statements - * @return \PhpParser\Node|null */ - public function findFunctionCallInStatements(array $functionNames, $statements): ?\PhpParser\Node + public function findFunctionCallInStatements(array $functionNames, $statements): ?Node { foreach ($statements as $statement) { if (is_array($statement)) { @@ -23,7 +25,7 @@ public function findFunctionCallInStatements(array $functionNames, $statements): } } - if (!($statement instanceof \PhpParser\Node)) { + if (!($statement instanceof Node)) { continue; } diff --git a/src/Parser/LexerFactory.php b/src/Parser/LexerFactory.php index e7b8dae7ab..a2d52481f5 100644 --- a/src/Parser/LexerFactory.php +++ b/src/Parser/LexerFactory.php @@ -4,15 +4,13 @@ use PhpParser\Lexer; use PHPStan\Php\PhpVersion; +use const PHP_VERSION_ID; class LexerFactory { - private PhpVersion $phpVersion; - - public function __construct(PhpVersion $phpVersion) + public function __construct(private PhpVersion $phpVersion) { - $this->phpVersion = $phpVersion; } public function create(): Lexer diff --git a/src/Parser/NodeList.php b/src/Parser/NodeList.php index 18b52a456e..13e69eef13 100644 --- a/src/Parser/NodeList.php +++ b/src/Parser/NodeList.php @@ -7,14 +7,8 @@ class NodeList { - private Node $node; - - private ?self $next; - - public function __construct(Node $node, ?self $next = null) + public function __construct(private Node $node, private ?self $next = null) { - $this->node = $node; - $this->next = $next; } public function append(Node $node): self diff --git a/src/Parser/Parser.php b/src/Parser/Parser.php index 1346facd1d..187d6875c6 100644 --- a/src/Parser/Parser.php +++ b/src/Parser/Parser.php @@ -2,21 +2,22 @@ namespace PHPStan\Parser; +use PhpParser\Node; + /** @api */ interface Parser { /** * @param string $file path to a file to parse - * @return \PhpParser\Node\Stmt[] - * @throws \PHPStan\Parser\ParserErrorsException + * @return Node\Stmt[] + * @throws ParserErrorsException */ public function parseFile(string $file): array; /** - * @param string $sourceCode - * @return \PhpParser\Node\Stmt[] - * @throws \PHPStan\Parser\ParserErrorsException + * @return Node\Stmt[] + * @throws ParserErrorsException */ public function parseString(string $sourceCode): array; diff --git a/src/Parser/ParserErrorsException.php b/src/Parser/ParserErrorsException.php index 5be367a751..240be67455 100644 --- a/src/Parser/ParserErrorsException.php +++ b/src/Parser/ParserErrorsException.php @@ -2,34 +2,27 @@ namespace PHPStan\Parser; +use Exception; use PhpParser\Error; +use function array_map; +use function implode; -class ParserErrorsException extends \Exception +class ParserErrorsException extends Exception { - /** @var \PhpParser\Error[] */ - private array $errors; - - private ?string $parsedFile; - /** - * @param \PhpParser\Error[] $errors - * @param string|null $parsedFile + * @param Error[] $errors */ public function __construct( - array $errors, - ?string $parsedFile + private array $errors, + private ?string $parsedFile, ) { - parent::__construct(implode(', ', array_map(static function (Error $error): string { - return $error->getMessage(); - }, $errors))); - $this->errors = $errors; - $this->parsedFile = $parsedFile; + parent::__construct(implode(', ', array_map(static fn (Error $error): string => $error->getMessage(), $errors))); } /** - * @return \PhpParser\Error[] + * @return Error[] */ public function getErrors(): array { diff --git a/src/Parser/PathRoutingParser.php b/src/Parser/PathRoutingParser.php index 7fa5f6aba3..9420459618 100644 --- a/src/Parser/PathRoutingParser.php +++ b/src/Parser/PathRoutingParser.php @@ -3,38 +3,43 @@ namespace PHPStan\Parser; use PHPStan\File\FileHelper; +use function array_fill_keys; +use function strpos; class PathRoutingParser implements Parser { - private FileHelper $fileHelper; - - private Parser $currentPhpVersionRichParser; - - private Parser $currentPhpVersionSimpleParser; - - private Parser $php8Parser; + /** @var bool[] filePath(string) => bool(true) */ + private array $analysedFiles = []; public function __construct( - FileHelper $fileHelper, - Parser $currentPhpVersionRichParser, - Parser $currentPhpVersionSimpleParser, - Parser $php8Parser + private FileHelper $fileHelper, + private Parser $currentPhpVersionRichParser, + private Parser $currentPhpVersionSimpleParser, + private Parser $php8Parser, ) { - $this->fileHelper = $fileHelper; - $this->currentPhpVersionRichParser = $currentPhpVersionRichParser; - $this->currentPhpVersionSimpleParser = $currentPhpVersionSimpleParser; - $this->php8Parser = $php8Parser; + } + + /** + * @param string[] $files + */ + public function setAnalysedFiles(array $files): void + { + $this->analysedFiles = array_fill_keys($files, true); } public function parseFile(string $file): array { - $file = $this->fileHelper->normalizePath($file, '/'); - if (strpos($file, 'vendor/jetbrains/phpstorm-stubs') !== false) { + if (strpos($this->fileHelper->normalizePath($file, '/'), 'vendor/jetbrains/phpstorm-stubs') !== false) { return $this->php8Parser->parseFile($file); } + $file = $this->fileHelper->normalizePath($file); + if (!isset($this->analysedFiles[$file])) { + return $this->currentPhpVersionSimpleParser->parseFile($file); + } + return $this->currentPhpVersionRichParser->parseFile($file); } diff --git a/src/Parser/PhpParserDecorator.php b/src/Parser/PhpParserDecorator.php index 6719215d69..d50ddee17b 100644 --- a/src/Parser/PhpParserDecorator.php +++ b/src/Parser/PhpParserDecorator.php @@ -2,33 +2,32 @@ namespace PHPStan\Parser; +use PhpParser\Error; use PhpParser\ErrorHandler; +use PhpParser\Node; +use PhpParser\Parser; +use function sprintf; -class PhpParserDecorator implements \PhpParser\Parser +class PhpParserDecorator implements Parser { - private \PHPStan\Parser\Parser $wrappedParser; - - public function __construct(\PHPStan\Parser\Parser $wrappedParser) + public function __construct(private \PHPStan\Parser\Parser $wrappedParser) { - $this->wrappedParser = $wrappedParser; } /** - * @param string $code - * @param \PhpParser\ErrorHandler|null $errorHandler - * @return \PhpParser\Node\Stmt[] + * @return Node\Stmt[] */ public function parse(string $code, ?ErrorHandler $errorHandler = null): array { try { return $this->wrappedParser->parseString($code); - } catch (\PHPStan\Parser\ParserErrorsException $e) { + } catch (ParserErrorsException $e) { $message = $e->getMessage(); if ($e->getParsedFile() !== null) { $message .= sprintf(' in file %s', $e->getParsedFile()); } - throw new \PhpParser\Error($message); + throw new Error($message); } } diff --git a/src/Parser/RemoveUnusedCodeByPhpVersionIdVisitor.php b/src/Parser/RemoveUnusedCodeByPhpVersionIdVisitor.php new file mode 100644 index 0000000000..076ddcccfd --- /dev/null +++ b/src/Parser/RemoveUnusedCodeByPhpVersionIdVisitor.php @@ -0,0 +1,106 @@ +elseifs) > 0) { + return null; + } + + if ($node->else === null) { + return null; + } + + $cond = $node->cond; + if ( + !$cond instanceof Node\Expr\BinaryOp\Smaller + && !$cond instanceof Node\Expr\BinaryOp\SmallerOrEqual + && !$cond instanceof Node\Expr\BinaryOp\Greater + && !$cond instanceof Node\Expr\BinaryOp\GreaterOrEqual + && !$cond instanceof Node\Expr\BinaryOp\Equal + && !$cond instanceof Node\Expr\BinaryOp\NotEqual + && !$cond instanceof Node\Expr\BinaryOp\Identical + && !$cond instanceof Node\Expr\BinaryOp\NotIdentical + ) { + return null; + } + + $operator = $cond->getOperatorSigil(); + if ($operator === '===') { + $operator = '=='; + } + if ($operator === '!==') { + $operator = '!='; + } + + $operands = $this->getOperands($cond->left, $cond->right); + if ($operands === null) { + return null; + } + + $result = version_compare($operands[0], $operands[1], $operator); + if ($result) { + // remove else + $node->cond = new Node\Expr\ConstFetch(new Node\Name('true')); + $node->else = null; + + return $node; + } + + // remove if + $node->cond = new Node\Expr\ConstFetch(new Node\Name('false')); + $node->stmts = []; + + return $node; + } + + /** + * @return array{string, string}|null + */ + private function getOperands(Node\Expr $left, Node\Expr $right): ?array + { + if ( + $left instanceof Node\Scalar\LNumber + && $right instanceof Node\Expr\ConstFetch + && $right->name->toString() === 'PHP_VERSION_ID' + ) { + return [(new PhpVersion($left->value))->getVersionString(), $this->phpVersionString]; + } + + if ( + $right instanceof Node\Scalar\LNumber + && $left instanceof Node\Expr\ConstFetch + && $left->name->toString() === 'PHP_VERSION_ID' + ) { + return [$this->phpVersionString, (new PhpVersion($right->value))->getVersionString()]; + } + + return null; + } + +} diff --git a/src/Parser/RichParser.php b/src/Parser/RichParser.php index 2a5fc5d56c..51ae991940 100644 --- a/src/Parser/RichParser.php +++ b/src/Parser/RichParser.php @@ -3,62 +3,59 @@ namespace PHPStan\Parser; use PhpParser\ErrorHandler\Collecting; +use PhpParser\Lexer; +use PhpParser\Node; use PhpParser\NodeTraverser; use PhpParser\NodeVisitor\NameResolver; use PhpParser\NodeVisitor\NodeConnectingVisitor; use PHPStan\File\FileReader; use PHPStan\NodeVisitor\StatementOrderVisitor; +use PHPStan\ShouldNotHappenException; +use function is_string; +use function strpos; +use function substr_count; +use const T_COMMENT; +use const T_DOC_COMMENT; class RichParser implements Parser { - private \PhpParser\Parser $parser; - - private NameResolver $nameResolver; - - private NodeConnectingVisitor $nodeConnectingVisitor; - - private StatementOrderVisitor $statementOrderVisitor; - public function __construct( - \PhpParser\Parser $parser, - NameResolver $nameResolver, - NodeConnectingVisitor $nodeConnectingVisitor, - StatementOrderVisitor $statementOrderVisitor + private \PhpParser\Parser $parser, + private Lexer $lexer, + private NameResolver $nameResolver, + private NodeConnectingVisitor $nodeConnectingVisitor, + private StatementOrderVisitor $statementOrderVisitor, ) { - $this->parser = $parser; - $this->nameResolver = $nameResolver; - $this->nodeConnectingVisitor = $nodeConnectingVisitor; - $this->statementOrderVisitor = $statementOrderVisitor; } /** * @param string $file path to a file to parse - * @return \PhpParser\Node\Stmt[] + * @return Node\Stmt[] */ public function parseFile(string $file): array { try { return $this->parseString(FileReader::read($file)); - } catch (\PHPStan\Parser\ParserErrorsException $e) { - throw new \PHPStan\Parser\ParserErrorsException($e->getErrors(), $file); + } catch (ParserErrorsException $e) { + throw new ParserErrorsException($e->getErrors(), $file); } } /** - * @param string $sourceCode - * @return \PhpParser\Node\Stmt[] + * @return Node\Stmt[] */ public function parseString(string $sourceCode): array { $errorHandler = new Collecting(); $nodes = $this->parser->parse($sourceCode, $errorHandler); + $tokens = $this->lexer->getTokens(); if ($errorHandler->hasErrors()) { - throw new \PHPStan\Parser\ParserErrorsException($errorHandler->getErrors(), null); + throw new ParserErrorsException($errorHandler->getErrors(), null); } if ($nodes === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $nodeTraverser = new NodeTraverser(); @@ -66,8 +63,46 @@ public function parseString(string $sourceCode): array $nodeTraverser->addVisitor($this->nodeConnectingVisitor); $nodeTraverser->addVisitor($this->statementOrderVisitor); - /** @var array<\PhpParser\Node\Stmt> */ - return $nodeTraverser->traverse($nodes); + /** @var array */ + $nodes = $nodeTraverser->traverse($nodes); + if (isset($nodes[0])) { + $nodes[0]->setAttribute('linesToIgnore', $this->getLinesToIgnore($tokens)); + } + + return $nodes; + } + + /** + * @param mixed[] $tokens + * @return int[] + */ + private function getLinesToIgnore(array $tokens): array + { + $lines = []; + foreach ($tokens as $token) { + if (is_string($token)) { + continue; + } + + $type = $token[0]; + if ($type !== T_COMMENT && $type !== T_DOC_COMMENT) { + continue; + } + + $text = $token[1]; + $line = $token[2]; + if (strpos($text, '@phpstan-ignore-next-line') !== false) { + $line++; + } elseif (strpos($text, '@phpstan-ignore-line') === false) { + continue; + } + + $line += substr_count($token[1], "\n"); + + $lines[] = $line; + } + + return $lines; } } diff --git a/src/Parser/SimpleParser.php b/src/Parser/SimpleParser.php index f20603457b..efcf47d786 100644 --- a/src/Parser/SimpleParser.php +++ b/src/Parser/SimpleParser.php @@ -3,58 +3,53 @@ namespace PHPStan\Parser; use PhpParser\ErrorHandler\Collecting; +use PhpParser\Node; use PhpParser\NodeTraverser; use PhpParser\NodeVisitor\NameResolver; use PHPStan\File\FileReader; +use PHPStan\ShouldNotHappenException; class SimpleParser implements Parser { - private \PhpParser\Parser $parser; - - private NameResolver $nameResolver; - public function __construct( - \PhpParser\Parser $parser, - NameResolver $nameResolver + private \PhpParser\Parser $parser, + private NameResolver $nameResolver, ) { - $this->parser = $parser; - $this->nameResolver = $nameResolver; } /** * @param string $file path to a file to parse - * @return \PhpParser\Node\Stmt[] + * @return Node\Stmt[] */ public function parseFile(string $file): array { try { return $this->parseString(FileReader::read($file)); - } catch (\PHPStan\Parser\ParserErrorsException $e) { - throw new \PHPStan\Parser\ParserErrorsException($e->getErrors(), $file); + } catch (ParserErrorsException $e) { + throw new ParserErrorsException($e->getErrors(), $file); } } /** - * @param string $sourceCode - * @return \PhpParser\Node\Stmt[] + * @return Node\Stmt[] */ public function parseString(string $sourceCode): array { $errorHandler = new Collecting(); $nodes = $this->parser->parse($sourceCode, $errorHandler); if ($errorHandler->hasErrors()) { - throw new \PHPStan\Parser\ParserErrorsException($errorHandler->getErrors(), null); + throw new ParserErrorsException($errorHandler->getErrors(), null); } if ($nodes === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $nodeTraverser = new NodeTraverser(); $nodeTraverser->addVisitor($this->nameResolver); - /** @var array<\PhpParser\Node\Stmt> */ + /** @var array */ return $nodeTraverser->traverse($nodes); } diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index a43b6d7d79..e565cf227a 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -2,15 +2,14 @@ namespace PHPStan\Php; +use function floor; + /** @api */ class PhpVersion { - private int $versionId; - - public function __construct(int $versionId) + public function __construct(private int $versionId) { - $this->versionId = $versionId; } public function getVersionId(): int @@ -97,9 +96,89 @@ public function throwsTypeErrorForInternalFunctions(): bool return $this->versionId >= 80000; } + public function throwsValueErrorForInternalFunctions(): bool + { + return $this->versionId >= 80000; + } + public function supportsHhPrintfSpecifier(): bool { return $this->versionId >= 80000; } + public function isEmptyStringValidAliasForNoneInMbSubstituteCharacter(): bool + { + return $this->versionId < 80000; + } + + public function supportsAllUnicodeScalarCodePointsInMbSubstituteCharacter(): bool + { + return $this->versionId >= 70200; + } + + public function isNumericStringValidArgInMbSubstituteCharacter(): bool + { + return $this->versionId < 80000; + } + + public function isNullValidArgInMbSubstituteCharacter(): bool + { + return $this->versionId >= 80000; + } + + public function isInterfaceConstantImplicitlyFinal(): bool + { + return $this->versionId < 80100; + } + + public function supportsFinalConstants(): bool + { + return $this->versionId >= 80100; + } + + public function supportsReadOnlyProperties(): bool + { + return $this->versionId >= 80100; + } + + public function supportsEnums(): bool + { + return $this->versionId >= 80100; + } + + public function supportsPureIntersectionTypes(): bool + { + return $this->versionId >= 80100; + } + + public function supportsCaseInsensitiveConstantNames(): bool + { + return $this->versionId < 80000; + } + + public function hasStricterRoundFunctions(): bool + { + return $this->versionId >= 80000; + } + + public function hasTentativeReturnTypes(): bool + { + return $this->versionId >= 80100; + } + + public function supportsFirstClassCallables(): bool + { + return $this->versionId >= 80100; + } + + public function supportsArrayUnpackingWithStringKeys(): bool + { + return $this->versionId >= 80100; + } + + public function throwsOnInvalidMbStringEncoding(): bool + { + return $this->versionId >= 80000; + } + } diff --git a/src/Php/PhpVersionFactory.php b/src/Php/PhpVersionFactory.php index 8d726313fd..fcf77a16ec 100644 --- a/src/Php/PhpVersionFactory.php +++ b/src/Php/PhpVersionFactory.php @@ -2,22 +2,19 @@ namespace PHPStan\Php; +use function explode; +use function max; +use function min; use const PHP_VERSION_ID; class PhpVersionFactory { - private ?int $versionId; - - private ?string $composerPhpVersion; - public function __construct( - ?int $versionId, - ?string $composerPhpVersion + private ?int $versionId, + private ?string $composerPhpVersion, ) { - $this->versionId = $versionId; - $this->composerPhpVersion = $composerPhpVersion; } public function create(): PhpVersion @@ -27,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, 80099); + $versionId = min($tmp, 80199); } if ($versionId === null) { diff --git a/src/Php/PhpVersionFactoryFactory.php b/src/Php/PhpVersionFactoryFactory.php index 92fd1aa675..870d1ff276 100644 --- a/src/Php/PhpVersionFactoryFactory.php +++ b/src/Php/PhpVersionFactoryFactory.php @@ -3,37 +3,31 @@ namespace PHPStan\Php; use Nette\Utils\Json; +use Nette\Utils\JsonException; +use PHPStan\File\CouldNotReadFileException; use PHPStan\File\FileReader; +use function count; +use function end; +use function is_file; +use function is_string; class PhpVersionFactoryFactory { - private ?int $versionId; - - private bool $readComposerPhpVersion; - - /** @var string[] */ - private array $composerAutoloaderProjectPaths; - /** - * @param bool $readComposerPhpVersion * @param string[] $composerAutoloaderProjectPaths */ public function __construct( - ?int $versionId, - bool $readComposerPhpVersion, - array $composerAutoloaderProjectPaths + private ?int $versionId, + private array $composerAutoloaderProjectPaths, ) { - $this->versionId = $versionId; - $this->readComposerPhpVersion = $readComposerPhpVersion; - $this->composerAutoloaderProjectPaths = $composerAutoloaderProjectPaths; } public function create(): PhpVersionFactory { $composerPhpVersion = null; - if ($this->readComposerPhpVersion && count($this->composerAutoloaderProjectPaths) > 0) { + if (count($this->composerAutoloaderProjectPaths) > 0) { $composerJsonPath = end($this->composerAutoloaderProjectPaths) . '/composer.json'; if (is_file($composerJsonPath)) { try { @@ -43,7 +37,7 @@ public function create(): PhpVersionFactory if (is_string($platformVersion)) { $composerPhpVersion = $platformVersion; } - } catch (\PHPStan\File\CouldNotReadFileException | \Nette\Utils\JsonException $e) { + } catch (CouldNotReadFileException | JsonException) { // pass } } diff --git a/src/PhpDoc/DirectTypeNodeResolverExtensionRegistryProvider.php b/src/PhpDoc/DirectTypeNodeResolverExtensionRegistryProvider.php new file mode 100644 index 0000000000..cd912d76bb --- /dev/null +++ b/src/PhpDoc/DirectTypeNodeResolverExtensionRegistryProvider.php @@ -0,0 +1,17 @@ +registry; + } + +} diff --git a/src/PhpDoc/LazyTypeNodeResolverExtensionRegistryProvider.php b/src/PhpDoc/LazyTypeNodeResolverExtensionRegistryProvider.php index f6cf2220ff..4a041e369e 100644 --- a/src/PhpDoc/LazyTypeNodeResolverExtensionRegistryProvider.php +++ b/src/PhpDoc/LazyTypeNodeResolverExtensionRegistryProvider.php @@ -2,24 +2,23 @@ namespace PHPStan\PhpDoc; +use PHPStan\DependencyInjection\Container; + class LazyTypeNodeResolverExtensionRegistryProvider implements TypeNodeResolverExtensionRegistryProvider { - private \PHPStan\DependencyInjection\Container $container; - private ?TypeNodeResolverExtensionRegistry $registry = null; - public function __construct(\PHPStan\DependencyInjection\Container $container) + public function __construct(private Container $container) { - $this->container = $container; } public function getRegistry(): TypeNodeResolverExtensionRegistry { if ($this->registry === null) { - $this->registry = new TypeNodeResolverExtensionRegistry( + $this->registry = new TypeNodeResolverExtensionAwareRegistry( $this->container->getByType(TypeNodeResolver::class), - $this->container->getServicesByTag(TypeNodeResolverExtension::EXTENSION_TAG) + $this->container->getServicesByTag(TypeNodeResolverExtension::EXTENSION_TAG), ); } diff --git a/src/PhpDoc/NameScopedPhpDocString.php b/src/PhpDoc/NameScopedPhpDocString.php deleted file mode 100644 index 3de6dd2194..0000000000 --- a/src/PhpDoc/NameScopedPhpDocString.php +++ /dev/null @@ -1,42 +0,0 @@ -phpDocString = $phpDocString; - $this->nameScope = $nameScope; - } - - public function getPhpDocString(): string - { - return $this->phpDocString; - } - - public function getNameScope(): NameScope - { - return $this->nameScope; - } - - /** - * @param mixed[] $properties - * @return self - */ - public static function __set_state(array $properties): self - { - return new self( - $properties['phpDocString'], - $properties['nameScope'] - ); - } - -} diff --git a/src/PhpDoc/PhpDocBlock.php b/src/PhpDoc/PhpDocBlock.php index 880e8a92b4..d82dfda65c 100644 --- a/src/PhpDoc/PhpDocBlock.php +++ b/src/PhpDoc/PhpDocBlock.php @@ -3,57 +3,34 @@ namespace PHPStan\PhpDoc; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\Php\PhpMethodReflection; use PHPStan\Reflection\Php\PhpPropertyReflection; +use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\ResolvedMethodReflection; use PHPStan\Reflection\ResolvedPropertyReflection; +use function array_key_exists; +use function count; +use function strtolower; class PhpDocBlock { - private string $docComment; - - private string $file; - - private ClassReflection $classReflection; - - private ?string $trait; - - private bool $explicit; - - /** @var array */ - private array $parameterNameMapping; - - /** @var array */ - private array $parents; - /** - * @param string $docComment - * @param string $file - * @param \PHPStan\Reflection\ClassReflection $classReflection - * @param string|null $trait - * @param bool $explicit * @param array $parameterNameMapping * @param array $parents */ private function __construct( - string $docComment, - string $file, - ClassReflection $classReflection, - ?string $trait, - bool $explicit, - array $parameterNameMapping, - array $parents + private string $docComment, + private string $file, + private ClassReflection $classReflection, + private ?string $trait, + private bool $explicit, + private array $parameterNameMapping, + private array $parents, ) { - $this->docComment = $docComment; - $this->file = $file; - $this->classReflection = $classReflection; - $this->trait = $trait; - $this->explicit = $explicit; - $this->parameterNameMapping = $parameterNameMapping; - $this->parents = $parents; } public function getDocComment(): string @@ -108,15 +85,8 @@ public function transformArrayKeysWithParameterNameMapping(array $array): array } /** - * @param string|null $docComment - * @param \PHPStan\Reflection\ClassReflection $classReflection - * @param string|null $trait - * @param string $propertyName - * @param string $file - * @param bool|null $explicit * @param array $originalPositionalParameterNames * @param array $newPositionalParameterNames - * @return self */ public static function resolvePhpDocBlockForProperty( ?string $docComment, @@ -126,7 +96,7 @@ public static function resolvePhpDocBlockForProperty( string $file, ?bool $explicit, array $originalPositionalParameterNames, // unused - array $newPositionalParameterNames // unused + array $newPositionalParameterNames, // unused ): self { return self::resolvePhpDocBlockTree( @@ -140,20 +110,43 @@ public static function resolvePhpDocBlockForProperty( __FUNCTION__, $explicit, [], - [] + [], + ); + } + + /** + * @param array $originalPositionalParameterNames + * @param array $newPositionalParameterNames + */ + public static function resolvePhpDocBlockForConstant( + ?string $docComment, + ClassReflection $classReflection, + ?string $trait, // unused + string $constantName, + string $file, + ?bool $explicit, + array $originalPositionalParameterNames, // unused + array $newPositionalParameterNames, // unused + ): self + { + return self::resolvePhpDocBlockTree( + $docComment, + $classReflection, + null, + $constantName, + $file, + 'hasConstant', + 'getConstant', + __FUNCTION__, + $explicit, + [], + [], ); } /** - * @param string|null $docComment - * @param \PHPStan\Reflection\ClassReflection $classReflection - * @param string|null $trait - * @param string $methodName - * @param string $file - * @param bool|null $explicit * @param array $originalPositionalParameterNames * @param array $newPositionalParameterNames - * @return self */ public static function resolvePhpDocBlockForMethod( ?string $docComment, @@ -163,7 +156,7 @@ public static function resolvePhpDocBlockForMethod( string $file, ?bool $explicit, array $originalPositionalParameterNames, - array $newPositionalParameterNames + array $newPositionalParameterNames, ): self { return self::resolvePhpDocBlockTree( @@ -177,23 +170,13 @@ public static function resolvePhpDocBlockForMethod( __FUNCTION__, $explicit, $originalPositionalParameterNames, - $newPositionalParameterNames + $newPositionalParameterNames, ); } /** - * @param string|null $docComment - * @param \PHPStan\Reflection\ClassReflection $classReflection - * @param string|null $trait - * @param string $name - * @param string $file - * @param string $hasMethodName - * @param string $getMethodName - * @param string $resolveMethodName - * @param bool|null $explicit * @param array $originalPositionalParameterNames * @param array $newPositionalParameterNames - * @return self */ private static function resolvePhpDocBlockTree( ?string $docComment, @@ -206,7 +189,7 @@ private static function resolvePhpDocBlockTree( string $resolveMethodName, ?bool $explicit, array $originalPositionalParameterNames, - array $newPositionalParameterNames + array $newPositionalParameterNames, ): self { $docBlocksFromParents = self::resolveParentPhpDocBlocks( @@ -216,7 +199,7 @@ private static function resolvePhpDocBlockTree( $getMethodName, $resolveMethodName, $explicit ?? $docComment !== null, - $newPositionalParameterNames + $newPositionalParameterNames, ); return new self( @@ -226,7 +209,7 @@ private static function resolvePhpDocBlockTree( $trait, $explicit ?? true, self::remapParameterNames($originalPositionalParameterNames, $newPositionalParameterNames), - $docBlocksFromParents + $docBlocksFromParents, ); } @@ -237,7 +220,7 @@ private static function resolvePhpDocBlockTree( */ private static function remapParameterNames( array $originalPositionalParameterNames, - array $newPositionalParameterNames + array $newPositionalParameterNames, ): array { $parameterNameMapping = []; @@ -252,12 +235,6 @@ private static function remapParameterNames( } /** - * @param ClassReflection $classReflection - * @param string $name - * @param string $hasMethodName - * @param string $getMethodName - * @param string $resolveMethodName - * @param bool $explicit * @param array $positionalParameterNames * @return array */ @@ -268,7 +245,7 @@ private static function resolveParentPhpDocBlocks( string $getMethodName, string $resolveMethodName, bool $explicit, - array $positionalParameterNames + array $positionalParameterNames, ): array { $result = []; @@ -282,7 +259,7 @@ private static function resolveParentPhpDocBlocks( $getMethodName, $resolveMethodName, $explicit, - $positionalParameterNames + $positionalParameterNames, ); if ($oneResult === null) { // Null if it is private or from a wrong trait. @@ -296,7 +273,6 @@ private static function resolveParentPhpDocBlocks( } /** - * @param ClassReflection $classReflection * @return array */ private static function getParentReflections(ClassReflection $classReflection): array @@ -304,7 +280,7 @@ private static function getParentReflections(ClassReflection $classReflection): $result = []; $parent = $classReflection->getParentClass(); - if ($parent !== false) { + if ($parent !== null) { $result[] = $parent; } @@ -316,14 +292,7 @@ private static function getParentReflections(ClassReflection $classReflection): } /** - * @param \PHPStan\Reflection\ClassReflection $classReflection - * @param string $name - * @param string $hasMethodName - * @param string $getMethodName - * @param string $resolveMethodName - * @param bool $explicit * @param array $positionalParameterNames - * @return self|null */ private static function resolvePhpDocBlockFromClass( ClassReflection $classReflection, @@ -332,11 +301,11 @@ private static function resolvePhpDocBlockFromClass( string $getMethodName, string $resolveMethodName, bool $explicit, - array $positionalParameterNames + array $positionalParameterNames, ): ?self { - if ($classReflection->getFileNameWithPhpDocs() !== null && $classReflection->$hasMethodName($name)) { - /** @var \PHPStan\Reflection\PropertyReflection|\PHPStan\Reflection\MethodReflection $parentReflection */ + if ($classReflection->getFileName() !== null && $classReflection->$hasMethodName($name)) { + /** @var PropertyReflection|MethodReflection|ConstantReflection $parentReflection */ $parentReflection = $classReflection->$getMethodName($name); if ($parentReflection->isPrivate()) { return null; @@ -379,10 +348,10 @@ private static function resolvePhpDocBlockFromClass( $classReflection, $trait, $name, - $classReflection->getFileNameWithPhpDocs(), + $classReflection->getFileName(), $explicit, $positionalParameterNames, - $positionalMethodParameterNames + $positionalMethodParameterNames, ); } diff --git a/src/PhpDoc/PhpDocInheritanceResolver.php b/src/PhpDoc/PhpDocInheritanceResolver.php index 67c86af318..3d0b571146 100644 --- a/src/PhpDoc/PhpDocInheritanceResolver.php +++ b/src/PhpDoc/PhpDocInheritanceResolver.php @@ -4,17 +4,18 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Type\FileTypeMapper; +use ReflectionParameter; +use function array_map; +use function strtolower; class PhpDocInheritanceResolver { - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - public function __construct( - FileTypeMapper $fileTypeMapper + private FileTypeMapper $fileTypeMapper, + private StubPhpDocProvider $stubPhpDocProvider, ) { - $this->fileTypeMapper = $fileTypeMapper; } public function resolvePhpDocForProperty( @@ -22,7 +23,7 @@ public function resolvePhpDocForProperty( ClassReflection $classReflection, string $classReflectionFileName, ?string $declaringTraitName, - string $propertyName + string $propertyName, ): ResolvedPhpDocBlock { $phpDocBlock = PhpDocBlock::resolvePhpDocBlockForProperty( @@ -33,20 +34,35 @@ public function resolvePhpDocForProperty( $classReflectionFileName, null, [], - [] + [], ); - return $this->docBlockTreeToResolvedDocBlock($phpDocBlock, $declaringTraitName, null); + return $this->docBlockTreeToResolvedDocBlock($phpDocBlock, $declaringTraitName, null, $propertyName, null); + } + + public function resolvePhpDocForConstant( + ?string $docComment, + ClassReflection $classReflection, + string $classReflectionFileName, + string $constantName, + ): ResolvedPhpDocBlock + { + $phpDocBlock = PhpDocBlock::resolvePhpDocBlockForConstant( + $docComment, + $classReflection, + null, + $constantName, + $classReflectionFileName, + null, + [], + [], + ); + + return $this->docBlockTreeToResolvedDocBlock($phpDocBlock, null, null, null, $constantName); } /** - * @param string|null $docComment - * @param string $fileName - * @param ClassReflection $classReflection - * @param string|null $declaringTraitName - * @param string $methodName * @param array $positionalParameterNames - * @return ResolvedPhpDocBlock */ public function resolvePhpDocForMethod( ?string $docComment, @@ -54,7 +70,7 @@ public function resolvePhpDocForMethod( ClassReflection $classReflection, ?string $declaringTraitName, string $methodName, - array $positionalParameterNames + array $positionalParameterNames, ): ResolvedPhpDocBlock { $phpDocBlock = PhpDocBlock::resolvePhpDocBlockForMethod( @@ -65,13 +81,13 @@ public function resolvePhpDocForMethod( $fileName, null, $positionalParameterNames, - $positionalParameterNames + $positionalParameterNames, ); - return $this->docBlockTreeToResolvedDocBlock($phpDocBlock, $phpDocBlock->getTrait(), $methodName); + return $this->docBlockTreeToResolvedDocBlock($phpDocBlock, $phpDocBlock->getTrait(), $methodName, null, null); } - private function docBlockTreeToResolvedDocBlock(PhpDocBlock $phpDocBlock, ?string $traitName, ?string $functionName): ResolvedPhpDocBlock + private function docBlockTreeToResolvedDocBlock(PhpDocBlock $phpDocBlock, ?string $traitName, ?string $functionName, ?string $propertyName, ?string $constantName): ResolvedPhpDocBlock { $parents = []; $parentPhpDocBlocks = []; @@ -87,25 +103,48 @@ private function docBlockTreeToResolvedDocBlock(PhpDocBlock $phpDocBlock, ?strin $parents[] = $this->docBlockTreeToResolvedDocBlock( $parentPhpDocBlock, $parentPhpDocBlock->getTrait(), - $functionName + $functionName, + $propertyName, + $constantName, ); $parentPhpDocBlocks[] = $parentPhpDocBlock; } - $oneResolvedDockBlock = $this->docBlockToResolvedDocBlock($phpDocBlock, $traitName, $functionName); + $oneResolvedDockBlock = $this->docBlockToResolvedDocBlock($phpDocBlock, $traitName, $functionName, $propertyName, $constantName); return $oneResolvedDockBlock->merge($parents, $parentPhpDocBlocks); } - private function docBlockToResolvedDocBlock(PhpDocBlock $phpDocBlock, ?string $traitName, ?string $functionName): ResolvedPhpDocBlock + private function docBlockToResolvedDocBlock(PhpDocBlock $phpDocBlock, ?string $traitName, ?string $functionName, ?string $propertyName, ?string $constantName): ResolvedPhpDocBlock { $classReflection = $phpDocBlock->getClassReflection(); + if ($functionName !== null && $classReflection->getNativeReflection()->hasMethod($functionName)) { + $methodReflection = $classReflection->getNativeReflection()->getMethod($functionName); + $stub = $this->stubPhpDocProvider->findMethodPhpDoc($classReflection->getName(), $functionName, array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $methodReflection->getParameters())); + if ($stub !== null) { + return $stub; + } + } + + if ($propertyName !== null && $classReflection->getNativeReflection()->hasProperty($propertyName)) { + $stub = $this->stubPhpDocProvider->findPropertyPhpDoc($classReflection->getName(), $propertyName); + if ($stub !== null) { + return $stub; + } + } + + if ($constantName !== null && $classReflection->getNativeReflection()->hasConstant($constantName)) { + $stub = $this->stubPhpDocProvider->findClassConstantPhpDoc($classReflection->getName(), $constantName); + if ($stub !== null) { + return $stub; + } + } return $this->fileTypeMapper->getResolvedPhpDoc( $phpDocBlock->getFile(), $classReflection->getName(), $traitName, $functionName, - $phpDocBlock->getDocComment() + $phpDocBlock->getDocComment(), ); } diff --git a/src/PhpDoc/PhpDocNodeResolver.php b/src/PhpDoc/PhpDocNodeResolver.php index 82cb4df018..4aa2730423 100644 --- a/src/PhpDoc/PhpDocNodeResolver.php +++ b/src/PhpDoc/PhpDocNodeResolver.php @@ -28,31 +28,26 @@ use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function array_map; +use function array_reverse; +use function count; +use function in_array; +use function strpos; +use function substr; class PhpDocNodeResolver { - private TypeNodeResolver $typeNodeResolver; - - private ConstExprNodeResolver $constExprNodeResolver; - - private UnresolvableTypeHelper $unresolvableTypeHelper; - public function __construct( - TypeNodeResolver $typeNodeResolver, - ConstExprNodeResolver $constExprNodeResolver, - UnresolvableTypeHelper $unresolvableTypeHelper + private TypeNodeResolver $typeNodeResolver, + private ConstExprNodeResolver $constExprNodeResolver, + private UnresolvableTypeHelper $unresolvableTypeHelper, ) { - $this->typeNodeResolver = $typeNodeResolver; - $this->constExprNodeResolver = $constExprNodeResolver; - $this->unresolvableTypeHelper = $unresolvableTypeHelper; } /** - * @param PhpDocNode $phpDocNode - * @param NameScope $nameScope - * @return array + * @return array<(string|int), VarTag> */ public function resolveVarTags(PhpDocNode $phpDocNode, NameScope $nameScope): array { @@ -89,54 +84,56 @@ public function resolveVarTags(PhpDocNode $phpDocNode, NameScope $nameScope): ar } /** - * @param PhpDocNode $phpDocNode - * @param NameScope $nameScope - * @return array + * @return array */ public function resolvePropertyTags(PhpDocNode $phpDocNode, NameScope $nameScope): array { $resolved = []; - foreach ($phpDocNode->getPropertyTagValues() as $tagValue) { - $propertyName = substr($tagValue->propertyName, 1); - $propertyType = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); + foreach (['@property', '@phpstan-property'] as $tagName) { + foreach ($phpDocNode->getPropertyTagValues($tagName) as $tagValue) { + $propertyName = substr($tagValue->propertyName, 1); + $propertyType = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); - $resolved[$propertyName] = new PropertyTag( - $propertyType, - true, - true - ); + $resolved[$propertyName] = new PropertyTag( + $propertyType, + true, + true, + ); + } } - foreach ($phpDocNode->getPropertyReadTagValues() as $tagValue) { - $propertyName = substr($tagValue->propertyName, 1); - $propertyType = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); + foreach (['@property-read', '@phpstan-property-read'] as $tagName) { + foreach ($phpDocNode->getPropertyReadTagValues($tagName) as $tagValue) { + $propertyName = substr($tagValue->propertyName, 1); + $propertyType = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); - $resolved[$propertyName] = new PropertyTag( - $propertyType, - true, - false - ); + $resolved[$propertyName] = new PropertyTag( + $propertyType, + true, + false, + ); + } } - foreach ($phpDocNode->getPropertyWriteTagValues() as $tagValue) { - $propertyName = substr($tagValue->propertyName, 1); - $propertyType = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); + foreach (['@property-write', '@phpstan-property-write'] as $tagName) { + foreach ($phpDocNode->getPropertyWriteTagValues($tagName) as $tagValue) { + $propertyName = substr($tagValue->propertyName, 1); + $propertyType = $this->typeNodeResolver->resolve($tagValue->type, $nameScope); - $resolved[$propertyName] = new PropertyTag( - $propertyType, - false, - true - ); + $resolved[$propertyName] = new PropertyTag( + $propertyType, + false, + true, + ); + } } return $resolved; } /** - * @param PhpDocNode $phpDocNode - * @param NameScope $nameScope - * @return array + * @return array */ public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope): array { @@ -165,7 +162,7 @@ public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope): : PassedByReference::createNo(), $parameterNode->isVariadic || $parameterNode->defaultValue !== null, $parameterNode->isVariadic, - $defaultValue + $defaultValue, ); } @@ -174,7 +171,7 @@ public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope): ? $this->typeNodeResolver->resolve($tagValue->returnType, $nameScope) : new MixedType(), $tagValue->isStatic, - $parameters + $parameters, ); } } @@ -183,7 +180,7 @@ public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope): } /** - * @return array + * @return array */ public function resolveExtendsTags(PhpDocNode $phpDocNode, NameScope $nameScope): array { @@ -191,8 +188,8 @@ public function resolveExtendsTags(PhpDocNode $phpDocNode, NameScope $nameScope) foreach (['@extends', '@template-extends', '@phpstan-extends'] as $tagName) { foreach ($phpDocNode->getExtendsTagValues($tagName) as $tagValue) { - $resolved[$tagValue->type->type->name] = new ExtendsTag( - $this->typeNodeResolver->resolve($tagValue->type, $nameScope) + $resolved[$nameScope->resolveStringName($tagValue->type->type->name)] = new ExtendsTag( + $this->typeNodeResolver->resolve($tagValue->type, $nameScope), ); } } @@ -201,7 +198,7 @@ public function resolveExtendsTags(PhpDocNode $phpDocNode, NameScope $nameScope) } /** - * @return array + * @return array */ public function resolveImplementsTags(PhpDocNode $phpDocNode, NameScope $nameScope): array { @@ -209,8 +206,8 @@ public function resolveImplementsTags(PhpDocNode $phpDocNode, NameScope $nameSco foreach (['@implements', '@template-implements', '@phpstan-implements'] as $tagName) { foreach ($phpDocNode->getImplementsTagValues($tagName) as $tagValue) { - $resolved[$tagValue->type->type->name] = new ImplementsTag( - $this->typeNodeResolver->resolve($tagValue->type, $nameScope) + $resolved[$nameScope->resolveStringName($tagValue->type->type->name)] = new ImplementsTag( + $this->typeNodeResolver->resolve($tagValue->type, $nameScope), ); } } @@ -219,7 +216,7 @@ public function resolveImplementsTags(PhpDocNode $phpDocNode, NameScope $nameSco } /** - * @return array + * @return array */ public function resolveUsesTags(PhpDocNode $phpDocNode, NameScope $nameScope): array { @@ -227,8 +224,8 @@ public function resolveUsesTags(PhpDocNode $phpDocNode, NameScope $nameScope): a foreach (['@use', '@template-use', '@phpstan-use'] as $tagName) { foreach ($phpDocNode->getUsesTagValues($tagName) as $tagValue) { - $resolved[$tagValue->type->type->name] = new UsesTag( - $this->typeNodeResolver->resolve($tagValue->type, $nameScope) + $resolved[$nameScope->resolveStringName($tagValue->type->type->name)] = new UsesTag( + $this->typeNodeResolver->resolve($tagValue->type, $nameScope), ); } } @@ -237,9 +234,7 @@ public function resolveUsesTags(PhpDocNode $phpDocNode, NameScope $nameScope): a } /** - * @param PhpDocNode $phpDocNode - * @param NameScope $nameScope - * @return array + * @return array */ public function resolveTemplateTags(PhpDocNode $phpDocNode, NameScope $nameScope): array { @@ -284,8 +279,8 @@ public function resolveTemplateTags(PhpDocNode $phpDocNode, NameScope $nameScope $resolved[$valueNode->name] = new TemplateTag( $valueNode->name, - $valueNode->bound !== null ? $this->typeNodeResolver->resolve($valueNode->bound, $nameScope->unsetTemplateType($valueNode->name)) : new MixedType(), - $variance + $valueNode->bound !== null ? $this->typeNodeResolver->resolve($valueNode->bound, $nameScope->unsetTemplateType($valueNode->name)) : new MixedType(true), + $variance, ); $resolvedPrefix[$valueNode->name] = $prefix; } @@ -294,9 +289,7 @@ public function resolveTemplateTags(PhpDocNode $phpDocNode, NameScope $nameScope } /** - * @param PhpDocNode $phpDocNode - * @param NameScope $nameScope - * @return array + * @return array */ public function resolveParamTags(PhpDocNode $phpDocNode, NameScope $nameScope): array { @@ -312,7 +305,7 @@ public function resolveParamTags(PhpDocNode $phpDocNode, NameScope $nameScope): $resolved[$parameterName] = new ParamTag( $parameterType, - $tagValue->isVariadic + $tagValue->isVariadic, ); } } @@ -320,7 +313,7 @@ public function resolveParamTags(PhpDocNode $phpDocNode, NameScope $nameScope): return $resolved; } - public function resolveReturnTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?\PHPStan\PhpDoc\Tag\ReturnTag + public function resolveReturnTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?ReturnTag { $resolved = null; @@ -337,7 +330,7 @@ public function resolveReturnTag(PhpDocNode $phpDocNode, NameScope $nameScope): return $resolved; } - public function resolveThrowsTags(PhpDocNode $phpDocNode, NameScope $nameScope): ?\PHPStan\PhpDoc\Tag\ThrowsTag + public function resolveThrowsTags(PhpDocNode $phpDocNode, NameScope $nameScope): ?ThrowsTag { foreach (['@phpstan-throws', '@throws'] as $tagName) { $types = []; @@ -360,17 +353,13 @@ public function resolveThrowsTags(PhpDocNode $phpDocNode, NameScope $nameScope): } /** - * @param PhpDocNode $phpDocNode - * @param NameScope $nameScope * @return array */ public function resolveMixinTags(PhpDocNode $phpDocNode, NameScope $nameScope): array { - return array_map(function (MixinTagValueNode $mixinTagValueNode) use ($nameScope): MixinTag { - return new MixinTag( - $this->typeNodeResolver->resolve($mixinTagValueNode->type, $nameScope) - ); - }, $phpDocNode->getMixinTagValues()); + return array_map(fn (MixinTagValueNode $mixinTagValueNode): MixinTag => new MixinTag( + $this->typeNodeResolver->resolve($mixinTagValueNode->type, $nameScope), + ), $phpDocNode->getMixinTagValues()); } /** @@ -410,7 +399,7 @@ public function resolveTypeAliasImportTags(PhpDocNode $phpDocNode, NameScope $na return $resolved; } - public function resolveDeprecatedTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?\PHPStan\PhpDoc\Tag\DeprecatedTag + public function resolveDeprecatedTag(PhpDocNode $phpDocNode, NameScope $nameScope): ?DeprecatedTag { foreach ($phpDocNode->getDeprecatedTagValues() as $deprecatedTagValue) { $description = (string) $deprecatedTagValue; diff --git a/src/PhpDoc/PhpDocStringResolver.php b/src/PhpDoc/PhpDocStringResolver.php index b503e5cd13..73b968826d 100644 --- a/src/PhpDoc/PhpDocStringResolver.php +++ b/src/PhpDoc/PhpDocStringResolver.php @@ -10,14 +10,8 @@ class PhpDocStringResolver { - private Lexer $phpDocLexer; - - private PhpDocParser $phpDocParser; - - public function __construct(Lexer $phpDocLexer, PhpDocParser $phpDocParser) + public function __construct(private Lexer $phpDocLexer, private PhpDocParser $phpDocParser) { - $this->phpDocLexer = $phpDocLexer; - $this->phpDocParser = $phpDocParser; } public function resolve(string $phpDocString): PhpDocNode diff --git a/src/PhpDoc/ResolvedPhpDocBlock.php b/src/PhpDoc/ResolvedPhpDocBlock.php index bba06a95fd..38350d4cf4 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -3,17 +3,27 @@ namespace PHPStan\PhpDoc; use PHPStan\Analyser\NameScope; +use PHPStan\PhpDoc\Tag\DeprecatedTag; +use PHPStan\PhpDoc\Tag\ExtendsTag; +use PHPStan\PhpDoc\Tag\ImplementsTag; +use PHPStan\PhpDoc\Tag\MethodTag; use PHPStan\PhpDoc\Tag\MixinTag; use PHPStan\PhpDoc\Tag\ParamTag; +use PHPStan\PhpDoc\Tag\PropertyTag; use PHPStan\PhpDoc\Tag\ReturnTag; +use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\PhpDoc\Tag\ThrowsTag; use PHPStan\PhpDoc\Tag\TypeAliasImportTag; use PHPStan\PhpDoc\Tag\TypeAliasTag; use PHPStan\PhpDoc\Tag\TypedTag; +use PHPStan\PhpDoc\Tag\UsesTag; use PHPStan\PhpDoc\Tag\VarTag; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; +use function array_key_exists; +use function count; +use function is_bool; /** @api */ class ResolvedPhpDocBlock @@ -32,49 +42,46 @@ class ResolvedPhpDocBlock private TemplateTypeMap $templateTypeMap; - /** @var array */ + /** @var array */ private array $templateTags; - private \PHPStan\PhpDoc\PhpDocNodeResolver $phpDocNodeResolver; + private PhpDocNodeResolver $phpDocNodeResolver; - /** @var array|false */ - private $varTags = false; + /** @var array<(string|int), VarTag>|false */ + private array|false $varTags = false; - /** @var array|false */ - private $methodTags = false; + /** @var array|false */ + private array|false $methodTags = false; - /** @var array|false */ - private $propertyTags = false; + /** @var array|false */ + private array|false $propertyTags = false; - /** @var array|false */ - private $extendsTags = false; + /** @var array|false */ + private array|false $extendsTags = false; - /** @var array|false */ - private $implementsTags = false; + /** @var array|false */ + private array|false $implementsTags = false; - /** @var array|false */ - private $usesTags = false; + /** @var array|false */ + private array|false $usesTags = false; - /** @var array|false */ - private $paramTags = false; + /** @var array|false */ + private array|false $paramTags = false; - /** @var \PHPStan\PhpDoc\Tag\ReturnTag|false|null */ - private $returnTag = false; + private ReturnTag|false|null $returnTag = false; - /** @var \PHPStan\PhpDoc\Tag\ThrowsTag|false|null */ - private $throwsTag = false; + private ThrowsTag|false|null $throwsTag = false; /** @var array|false */ - private $mixinTags = false; + private array|false $mixinTags = false; /** @var array|false */ - private $typeAliasTags = false; + private array|false $typeAliasTags = false; /** @var array|false */ - private $typeAliasImportTags = false; + private array|false $typeAliasImportTags = false; - /** @var \PHPStan\PhpDoc\Tag\DeprecatedTag|false|null */ - private $deprecatedTag = false; + private DeprecatedTag|false|null $deprecatedTag = false; private ?bool $isDeprecated = null; @@ -83,21 +90,14 @@ class ResolvedPhpDocBlock private ?bool $isFinal = null; /** @var bool|'notLoaded'|null */ - private $isPure = 'notLoaded'; + private bool|string|null $isPure = 'notLoaded'; private function __construct() { } /** - * @param \PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode $phpDocNode - * @param string $phpDocString - * @param string $filename - * @param \PHPStan\Analyser\NameScope $nameScope - * @param \PHPStan\Type\Generic\TemplateTypeMap $templateTypeMap - * @param \PHPStan\PhpDoc\Tag\TemplateTag[] $templateTags - * @param \PHPStan\PhpDoc\PhpDocNodeResolver $phpDocNodeResolver - * @return self + * @param TemplateTag[] $templateTags */ public static function create( PhpDocNode $phpDocNode, @@ -106,7 +106,7 @@ public static function create( NameScope $nameScope, TemplateTypeMap $templateTypeMap, array $templateTags, - PhpDocNodeResolver $phpDocNodeResolver + PhpDocNodeResolver $phpDocNodeResolver, ): self { // new property also needs to be added to createEmpty() and merge() @@ -156,7 +156,6 @@ public static function createEmpty(): self /** * @param array $parents * @param array $parentPhpDocBlocks - * @return self */ public function merge(array $parents, array $parentPhpDocBlocks): self { @@ -189,7 +188,7 @@ public function merge(array $parents, array $parentPhpDocBlocks): self $result->mixinTags = $this->getMixinTags(); $result->typeAliasTags = $this->getTypeAliasTags(); $result->typeAliasImportTags = $this->getTypeAliasImportTags(); - $result->deprecatedTag = $this->getDeprecatedTag(); + $result->deprecatedTag = self::mergeDeprecatedTags($this->getDeprecatedTag(), $parents); $result->isDeprecated = $result->deprecatedTag !== null; $result->isInternal = $this->isInternal(); $result->isFinal = $this->isFinal(); @@ -200,10 +199,13 @@ public function merge(array $parents, array $parentPhpDocBlocks): self /** * @param array $parameterNameMapping - * @return self */ public function changeParameterNamesByMapping(array $parameterNameMapping): self { + if (count($this->phpDocNodes) === 0) { + return $this; + } + $paramTags = $this->getParamTags(); $newParamTags = []; @@ -273,49 +275,49 @@ public function getNullableNameScope(): ?NameScope } /** - * @return array + * @return array<(string|int), VarTag> */ public function getVarTags(): array { if ($this->varTags === false) { $this->varTags = $this->phpDocNodeResolver->resolveVarTags( $this->phpDocNode, - $this->getNameScope() + $this->getNameScope(), ); } return $this->varTags; } /** - * @return array + * @return array */ public function getMethodTags(): array { if ($this->methodTags === false) { $this->methodTags = $this->phpDocNodeResolver->resolveMethodTags( $this->phpDocNode, - $this->getNameScope() + $this->getNameScope(), ); } return $this->methodTags; } /** - * @return array + * @return array */ public function getPropertyTags(): array { if ($this->propertyTags === false) { $this->propertyTags = $this->phpDocNodeResolver->resolvePropertyTags( $this->phpDocNode, - $this->getNameScope() + $this->getNameScope(), ); } return $this->propertyTags; } /** - * @return array + * @return array */ public function getTemplateTags(): array { @@ -323,78 +325,78 @@ public function getTemplateTags(): array } /** - * @return array + * @return array */ public function getExtendsTags(): array { if ($this->extendsTags === false) { $this->extendsTags = $this->phpDocNodeResolver->resolveExtendsTags( $this->phpDocNode, - $this->getNameScope() + $this->getNameScope(), ); } return $this->extendsTags; } /** - * @return array + * @return array */ public function getImplementsTags(): array { if ($this->implementsTags === false) { $this->implementsTags = $this->phpDocNodeResolver->resolveImplementsTags( $this->phpDocNode, - $this->getNameScope() + $this->getNameScope(), ); } return $this->implementsTags; } /** - * @return array + * @return array */ public function getUsesTags(): array { if ($this->usesTags === false) { $this->usesTags = $this->phpDocNodeResolver->resolveUsesTags( $this->phpDocNode, - $this->getNameScope() + $this->getNameScope(), ); } return $this->usesTags; } /** - * @return array + * @return array */ public function getParamTags(): array { if ($this->paramTags === false) { $this->paramTags = $this->phpDocNodeResolver->resolveParamTags( $this->phpDocNode, - $this->getNameScope() + $this->getNameScope(), ); } return $this->paramTags; } - public function getReturnTag(): ?\PHPStan\PhpDoc\Tag\ReturnTag + public function getReturnTag(): ?ReturnTag { - if ($this->returnTag === false) { + if (is_bool($this->returnTag)) { $this->returnTag = $this->phpDocNodeResolver->resolveReturnTag( $this->phpDocNode, - $this->getNameScope() + $this->getNameScope(), ); } return $this->returnTag; } - public function getThrowsTag(): ?\PHPStan\PhpDoc\Tag\ThrowsTag + public function getThrowsTag(): ?ThrowsTag { - if ($this->throwsTag === false) { + if (is_bool($this->throwsTag)) { $this->throwsTag = $this->phpDocNodeResolver->resolveThrowsTags( $this->phpDocNode, - $this->getNameScope() + $this->getNameScope(), ); } return $this->throwsTag; @@ -408,7 +410,7 @@ public function getMixinTags(): array if ($this->mixinTags === false) { $this->mixinTags = $this->phpDocNodeResolver->resolveMixinTags( $this->phpDocNode, - $this->getNameScope() + $this->getNameScope(), ); } @@ -423,7 +425,7 @@ public function getTypeAliasTags(): array if ($this->typeAliasTags === false) { $this->typeAliasTags = $this->phpDocNodeResolver->resolveTypeAliasTags( $this->phpDocNode, - $this->getNameScope() + $this->getNameScope(), ); } @@ -438,19 +440,19 @@ public function getTypeAliasImportTags(): array if ($this->typeAliasImportTags === false) { $this->typeAliasImportTags = $this->phpDocNodeResolver->resolveTypeAliasImportTags( $this->phpDocNode, - $this->getNameScope() + $this->getNameScope(), ); } return $this->typeAliasImportTags; } - public function getDeprecatedTag(): ?\PHPStan\PhpDoc\Tag\DeprecatedTag + public function getDeprecatedTag(): ?DeprecatedTag { - if ($this->deprecatedTag === false) { + if (is_bool($this->deprecatedTag)) { $this->deprecatedTag = $this->phpDocNodeResolver->resolveDeprecatedTag( $this->phpDocNode, - $this->getNameScope() + $this->getNameScope(), ); } return $this->deprecatedTag; @@ -460,7 +462,7 @@ public function isDeprecated(): bool { if ($this->isDeprecated === null) { $this->isDeprecated = $this->phpDocNodeResolver->resolveIsDeprecated( - $this->phpDocNode + $this->phpDocNode, ); } return $this->isDeprecated; @@ -470,7 +472,7 @@ public function isInternal(): bool { if ($this->isInternal === null) { $this->isInternal = $this->phpDocNodeResolver->resolveIsInternal( - $this->phpDocNode + $this->phpDocNode, ); } return $this->isInternal; @@ -480,7 +482,7 @@ public function isFinal(): bool { if ($this->isFinal === null) { $this->isFinal = $this->phpDocNodeResolver->resolveIsFinal( - $this->phpDocNode + $this->phpDocNode, ); } return $this->isFinal; @@ -495,14 +497,14 @@ public function isPure(): ?bool { if ($this->isPure === 'notLoaded') { $pure = $this->phpDocNodeResolver->resolveIsPure( - $this->phpDocNode + $this->phpDocNode, ); if ($pure) { $this->isPure = true; return $this->isPure; } else { $impure = $this->phpDocNodeResolver->resolveIsImpure( - $this->phpDocNode + $this->phpDocNode, ); if ($impure) { $this->isPure = false; @@ -543,7 +545,6 @@ private static function mergeVarTags(array $varTags, array $parents, array $pare /** * @param ResolvedPhpDocBlock $parent - * @param PhpDocBlock $phpDocBlock * @return array|null */ private static function mergeOneParentVarTags(self $parent, PhpDocBlock $phpDocBlock): ?array @@ -573,7 +574,6 @@ private static function mergeParamTags(array $paramTags, array $parents, array $ /** * @param array $paramTags * @param ResolvedPhpDocBlock $parent - * @param PhpDocBlock $phpDocBlock * @return array */ private static function mergeOneParentParamTags(array $paramTags, self $parent, PhpDocBlock $phpDocBlock): array @@ -592,7 +592,6 @@ private static function mergeOneParentParamTags(array $paramTags, self $parent, } /** - * @param ReturnTag|null $returnTag * @param array $parents * @param array $parentPhpDocBlocks * @return ReturnTag|Null @@ -633,6 +632,25 @@ private static function mergeOneParentReturnTag(?ReturnTag $returnTag, self $par return self::resolveTemplateTypeInTag($parentReturnTag->toImplicit(), $phpDocBlock); } + /** + * @param array $parents + */ + private static function mergeDeprecatedTags(?DeprecatedTag $deprecatedTag, array $parents): ?DeprecatedTag + { + if ($deprecatedTag !== null) { + return $deprecatedTag; + } + foreach ($parents as $parent) { + $result = $parent->getDeprecatedTag(); + if ($result === null) { + continue; + } + return $result; + } + + return null; + } + /** * @param array $parents */ @@ -654,16 +672,15 @@ private static function mergeThrowsTags(?ThrowsTag $throwsTag, array $parents): } /** - * @template T of \PHPStan\PhpDoc\Tag\TypedTag + * @template T of TypedTag * @param T $tag - * @param PhpDocBlock $phpDocBlock * @return T */ private static function resolveTemplateTypeInTag(TypedTag $tag, PhpDocBlock $phpDocBlock): TypedTag { $type = TemplateTypeHelper::resolveTemplateTypes( $tag->getType(), - $phpDocBlock->getClassReflection()->getActiveTemplateTypeMap() + $phpDocBlock->getClassReflection()->getActiveTemplateTypeMap(), ); return $tag->withType($type); } diff --git a/src/PhpDoc/StubFilesExtension.php b/src/PhpDoc/StubFilesExtension.php new file mode 100644 index 0000000000..237f2a04d3 --- /dev/null +++ b/src/PhpDoc/StubFilesExtension.php @@ -0,0 +1,14 @@ + */ private array $classMap = []; /** @var array> */ private array $propertyMap = []; + /** @var array> */ + private array $constantMap = []; + /** @var array> */ private array $methodMap = []; @@ -46,6 +46,9 @@ class StubPhpDocProvider /** @var array> */ private array $knownPropertiesDocComments = []; + /** @var array> */ + private array $knownConstantsDocComments = []; + /** @var array> */ private array $knownMethodsDocComments = []; @@ -56,18 +59,15 @@ class StubPhpDocProvider private array $knownFunctionParameterNames = []; /** - * @param \PHPStan\Parser\Parser $parser * @param string[] $stubFiles */ public function __construct( - Parser $parser, - FileTypeMapper $fileTypeMapper, - array $stubFiles + private Parser $parser, + private FileTypeMapper $fileTypeMapper, + private Container $container, + private array $stubFiles, ) { - $this->parser = $parser; - $this->fileTypeMapper = $fileTypeMapper; - $this->stubFiles = $stubFiles; } public function findClassPhpDoc(string $className): ?ResolvedPhpDocBlock @@ -87,7 +87,7 @@ public function findClassPhpDoc(string $className): ?ResolvedPhpDocBlock $className, null, null, - $docComment + $docComment, ); return $this->classMap[$className]; @@ -113,7 +113,7 @@ public function findPropertyPhpDoc(string $className, string $propertyName): ?Re $className, null, null, - $docComment + $docComment, ); return $this->propertyMap[$className][$propertyName]; @@ -122,11 +122,34 @@ public function findPropertyPhpDoc(string $className, string $propertyName): ?Re return null; } + public function findClassConstantPhpDoc(string $className, string $constantName): ?ResolvedPhpDocBlock + { + if (!$this->isKnownClass($className)) { + return null; + } + + if (array_key_exists($constantName, $this->constantMap[$className])) { + return $this->constantMap[$className][$constantName]; + } + + if (array_key_exists($constantName, $this->knownConstantsDocComments[$className])) { + [$file, $docComment] = $this->knownConstantsDocComments[$className][$constantName]; + $this->constantMap[$className][$constantName] = $this->fileTypeMapper->getResolvedPhpDoc( + $file, + $className, + null, + null, + $docComment, + ); + + return $this->constantMap[$className][$constantName]; + } + + return null; + } + /** - * @param string $className - * @param string $methodName * @param array $positionalParameterNames - * @return \PHPStan\PhpDoc\ResolvedPhpDocBlock|null */ public function findMethodPhpDoc(string $className, string $methodName, array $positionalParameterNames): ?ResolvedPhpDocBlock { @@ -145,11 +168,11 @@ public function findMethodPhpDoc(string $className, string $methodName, array $p $className, null, $methodName, - $docComment + $docComment, ); if (!isset($this->knownMethodsParameterNames[$className][$methodName])) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $methodParameterNames = $this->knownMethodsParameterNames[$className][$methodName]; @@ -168,10 +191,8 @@ public function findMethodPhpDoc(string $className, string $methodName, array $p } /** - * @param string $functionName * @param array $positionalParameterNames - * @return ResolvedPhpDocBlock|null - * @throws \PHPStan\ShouldNotHappenException + * @throws ShouldNotHappenException */ public function findFunctionPhpDoc(string $functionName, array $positionalParameterNames): ?ResolvedPhpDocBlock { @@ -190,11 +211,11 @@ public function findFunctionPhpDoc(string $functionName, array $positionalParame null, null, $functionName, - $docComment + $docComment, ); if (!isset($this->knownFunctionParameterNames[$functionName])) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $functionParameterNames = $this->knownFunctionParameterNames[$functionName]; @@ -239,7 +260,7 @@ private function isKnownFunction(string $functionName): bool private function initializeKnownElements(): void { if ($this->initializing) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ($this->initialized) { return; @@ -248,7 +269,7 @@ private function initializeKnownElements(): void $this->initializing = true; try { - foreach ($this->stubFiles as $stubFile) { + foreach ($this->getStubFiles() as $stubFile) { $nodes = $this->parser->parseFile($stubFile); foreach ($nodes as $node) { $this->initializeKnownElementNode($stubFile, $node); @@ -260,6 +281,23 @@ private function initializeKnownElements(): void } } + /** + * @return string[] + */ + private function getStubFiles(): array + { + $stubFiles = $this->stubFiles; + $extensions = $this->container->getServicesByTag(StubFilesExtension::EXTENSION_TAG); + foreach ($extensions as $extension) { + $extensionFiles = $extension->getFiles(); + foreach ($extensionFiles as $extensionFile) { + $stubFiles[] = $extensionFile; + } + } + + return $stubFiles; + } + private function initializeKnownElementNode(string $stubFile, Node $node): void { if ($node instanceof Node\Stmt\Namespace_) { @@ -278,7 +316,7 @@ private function initializeKnownElementNode(string $stubFile, Node $node): void } $this->knownFunctionParameterNames[$functionName] = array_map(static function (Node\Param $param): string { if (!$param->var instanceof Variable || !is_string($param->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $param->var->name; @@ -288,7 +326,7 @@ private function initializeKnownElementNode(string $stubFile, Node $node): void return; } - if (!$node instanceof Class_ && !$node instanceof Interface_ && !$node instanceof Trait_) { + if (!$node instanceof Class_ && !$node instanceof Interface_ && !$node instanceof Trait_ && !$node instanceof Node\Stmt\Enum_) { return; } @@ -306,7 +344,9 @@ private function initializeKnownElementNode(string $stubFile, Node $node): void $this->methodMap[$className] = []; $this->propertyMap[$className] = []; + $this->constantMap[$className] = []; $this->knownPropertiesDocComments[$className] = []; + $this->knownConstantsDocComments[$className] = []; $this->knownMethodsDocComments[$className] = []; foreach ($node->stmts as $stmt) { @@ -319,6 +359,14 @@ private function initializeKnownElementNode(string $stubFile, Node $node): void } $this->knownPropertiesDocComments[$className][$property->name->toString()] = [$stubFile, $docComment->getText()]; } + } elseif ($stmt instanceof Node\Stmt\ClassConst) { + foreach ($stmt->consts as $const) { + if ($docComment === null) { + $this->constantMap[$className][$const->name->toString()] = null; + continue; + } + $this->knownConstantsDocComments[$className][$const->name->toString()] = [$stubFile, $docComment->getText()]; + } } elseif ($stmt instanceof Node\Stmt\ClassMethod) { if ($docComment === null) { $this->methodMap[$className][$stmt->name->toString()] = null; @@ -329,7 +377,7 @@ private function initializeKnownElementNode(string $stubFile, Node $node): void $this->knownMethodsDocComments[$className][$methodName] = [$stubFile, $docComment->getText()]; $this->knownMethodsParameterNames[$className][$methodName] = array_map(static function (Node\Param $param): string { if (!$param->var instanceof Variable || !is_string($param->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $param->var->name; diff --git a/src/PhpDoc/StubSourceLocatorFactory.php b/src/PhpDoc/StubSourceLocatorFactory.php index a3a1215c34..f4253c14cc 100644 --- a/src/PhpDoc/StubSourceLocatorFactory.php +++ b/src/PhpDoc/StubSourceLocatorFactory.php @@ -2,59 +2,39 @@ namespace PHPStan\PhpDoc; -use PHPStan\BetterReflection\Reflector\FunctionReflector; +use PhpParser\Parser; use PHPStan\BetterReflection\SourceLocator\Ast\Locator; use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; use PHPStan\BetterReflection\SourceLocator\Type\AggregateSourceLocator; use PHPStan\BetterReflection\SourceLocator\Type\MemoizingSourceLocator; use PHPStan\BetterReflection\SourceLocator\Type\PhpInternalSourceLocator; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; -use PHPStan\DependencyInjection\Container; use PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocatorRepository; class StubSourceLocatorFactory { - private \PhpParser\Parser $parser; - - private PhpStormStubsSourceStubber $phpStormStubsSourceStubber; - - private \PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocatorRepository $optimizedSingleFileSourceLocatorRepository; - - private \PHPStan\DependencyInjection\Container $container; - - /** @var string[] */ - private array $stubFiles; - /** * @param string[] $stubFiles */ public function __construct( - \PhpParser\Parser $parser, - PhpStormStubsSourceStubber $phpStormStubsSourceStubber, - OptimizedSingleFileSourceLocatorRepository $optimizedSingleFileSourceLocatorRepository, - Container $container, - array $stubFiles + private Parser $php8Parser, + private PhpStormStubsSourceStubber $phpStormStubsSourceStubber, + private OptimizedSingleFileSourceLocatorRepository $optimizedSingleFileSourceLocatorRepository, + private array $stubFiles, ) { - $this->parser = $parser; - $this->phpStormStubsSourceStubber = $phpStormStubsSourceStubber; - $this->optimizedSingleFileSourceLocatorRepository = $optimizedSingleFileSourceLocatorRepository; - $this->container = $container; - $this->stubFiles = $stubFiles; } public function create(): SourceLocator { $locators = []; - $astLocator = new Locator($this->parser, function (): FunctionReflector { - return $this->container->getService('stubFunctionReflector'); - }); + $astPhp8Locator = new Locator($this->php8Parser); foreach ($this->stubFiles as $stubFile) { $locators[] = $this->optimizedSingleFileSourceLocatorRepository->getOrCreate($stubFile); } - $locators[] = new PhpInternalSourceLocator($astLocator, $this->phpStormStubsSourceStubber); + $locators[] = new PhpInternalSourceLocator($astPhp8Locator, $this->phpStormStubsSourceStubber); return new MemoizingSourceLocator(new AggregateSourceLocator($locators)); } diff --git a/src/PhpDoc/StubValidator.php b/src/PhpDoc/StubValidator.php index 7b7e606389..5bbf713a50 100644 --- a/src/PhpDoc/StubValidator.php +++ b/src/PhpDoc/StubValidator.php @@ -8,9 +8,11 @@ use PHPStan\Broker\Broker; use PHPStan\DependencyInjection\Container; use PHPStan\DependencyInjection\DerivativeContainerFactory; +use PHPStan\Php\PhpVersion; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\Classes\ExistingClassesInClassImplementsRule; use PHPStan\Rules\Classes\ExistingClassesInInterfaceExtendsRule; @@ -21,6 +23,7 @@ use PHPStan\Rules\Functions\MissingFunctionReturnTypehintRule; use PHPStan\Rules\Generics\ClassAncestorsRule; use PHPStan\Rules\Generics\ClassTemplateTypeRule; +use PHPStan\Rules\Generics\CrossCheckInterfacesHelper; use PHPStan\Rules\Generics\FunctionSignatureVarianceRule; use PHPStan\Rules\Generics\FunctionTemplateTypeRule; use PHPStan\Rules\Generics\GenericAncestorsCheck; @@ -33,8 +36,10 @@ use PHPStan\Rules\Generics\TraitTemplateTypeRule; use PHPStan\Rules\Generics\VarianceCheck; use PHPStan\Rules\Methods\ExistingClassesInTypehintsRule; +use PHPStan\Rules\Methods\MethodSignatureRule; use PHPStan\Rules\Methods\MissingMethodParameterTypehintRule; use PHPStan\Rules\Methods\MissingMethodReturnTypehintRule; +use PHPStan\Rules\Methods\OverridingMethodRule; use PHPStan\Rules\MissingTypehintCheck; use PHPStan\Rules\PhpDoc\IncompatiblePhpDocTypeRule; use PHPStan\Rules\PhpDoc\IncompatiblePropertyPhpDocTypeRule; @@ -46,22 +51,23 @@ use PHPStan\Rules\Registry; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\ObjectType; +use Throwable; +use function array_fill_keys; +use function count; +use function sprintf; class StubValidator { - private \PHPStan\DependencyInjection\DerivativeContainerFactory $derivativeContainerFactory; - public function __construct( - DerivativeContainerFactory $derivativeContainerFactory + private DerivativeContainerFactory $derivativeContainerFactory, ) { - $this->derivativeContainerFactory = $derivativeContainerFactory; } /** * @param string[] $stubFiles - * @return \PHPStan\Analyser\Error[] + * @return Error[] */ public function validate(array $stubFiles, bool $debug): array { @@ -70,6 +76,7 @@ public function validate(array $stubFiles, bool $debug): array } $originalBroker = Broker::getInstance(); + $originalReflectionProvider = ReflectionProviderStaticAccessor::getInstance(); $container = $this->derivativeContainerFactory->create([ __DIR__ . '/../../conf/config.stubValidator.neon', ]); @@ -83,6 +90,9 @@ public function validate(array $stubFiles, bool $debug): array $nodeScopeResolver = $container->getByType(NodeScopeResolver::class); $nodeScopeResolver->setAnalysedFiles($stubFiles); + $pathRoutingParser = $container->getService('pathRoutingParser'); + $pathRoutingParser->setAnalysedFiles($stubFiles); + $analysedFiles = array_fill_keys($stubFiles, true); $errors = []; @@ -93,12 +103,12 @@ public function validate(array $stubFiles, bool $debug): array $analysedFiles, $ruleRegistry, static function (): void { - } + }, )->getErrors(); foreach ($tmpErrors as $tmpError) { - $errors[] = $tmpError->withoutTip(); + $errors[] = $tmpError->withoutTip()->doNotIgnore(); } - } catch (\Throwable $e) { + } catch (Throwable $e) { if ($debug) { throw $e; } @@ -109,6 +119,7 @@ static function (): void { } Broker::registerInstance($originalBroker); + ReflectionProviderStaticAccessor::registerInstance($originalReflectionProvider); ObjectType::resetCaches(); return $errors; @@ -126,8 +137,10 @@ private function getRuleRegistry(Container $container): Registry $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); - return new Registry([ + $rules = [ // level 0 new ExistingClassesInClassImplementsRule($classCaseSensitivityCheck, $reflectionProvider), new ExistingClassesInInterfaceExtendsRule($classCaseSensitivityCheck, $reflectionProvider), @@ -135,27 +148,28 @@ private function getRuleRegistry(Container $container): Registry new ExistingClassInTraitUseRule($classCaseSensitivityCheck, $reflectionProvider), new ExistingClassesInTypehintsRule($functionDefinitionCheck), new \PHPStan\Rules\Functions\ExistingClassesInTypehintsRule($functionDefinitionCheck), - new ExistingClassesInPropertiesRule($reflectionProvider, $classCaseSensitivityCheck, true, false), + new ExistingClassesInPropertiesRule($reflectionProvider, $classCaseSensitivityCheck, $unresolvableTypeHelper, $phpVersion, true, false), + new OverridingMethodRule($phpVersion, new MethodSignatureRule(true, true), true), // level 2 - new ClassAncestorsRule($fileTypeMapper, $genericAncestorsCheck), + new ClassAncestorsRule($genericAncestorsCheck, $crossCheckInterfacesHelper), new ClassTemplateTypeRule($templateTypeCheck), new FunctionTemplateTypeRule($fileTypeMapper, $templateTypeCheck), new FunctionSignatureVarianceRule($varianceCheck), - new InterfaceAncestorsRule($fileTypeMapper, $genericAncestorsCheck), - new InterfaceTemplateTypeRule($fileTypeMapper, $templateTypeCheck), + new InterfaceAncestorsRule($genericAncestorsCheck, $crossCheckInterfacesHelper), + new InterfaceTemplateTypeRule($templateTypeCheck), new MethodTemplateTypeRule($fileTypeMapper, $templateTypeCheck), new MethodSignatureVarianceRule($varianceCheck), new TraitTemplateTypeRule($fileTypeMapper, $templateTypeCheck), new IncompatiblePhpDocTypeRule( $fileTypeMapper, $genericObjectTypeCheck, - $unresolvableTypeHelper + $unresolvableTypeHelper, ), new IncompatiblePropertyPhpDocTypeRule($genericObjectTypeCheck, $unresolvableTypeHelper), new InvalidPhpDocTagValueRule( $container->getByType(Lexer::class), - $container->getByType(PhpDocParser::class) + $container->getByType(PhpDocParser::class), ), new InvalidThrowsPhpDocValueRule($fileTypeMapper), @@ -165,7 +179,9 @@ private function getRuleRegistry(Container $container): Registry new MissingMethodParameterTypehintRule($missingTypehintCheck), new MissingMethodReturnTypehintRule($missingTypehintCheck), new MissingPropertyTypehintRule($missingTypehintCheck), - ]); + ]; + + return new Registry($rules); } } diff --git a/src/PhpDoc/Tag/DeprecatedTag.php b/src/PhpDoc/Tag/DeprecatedTag.php index 1e6309658f..ea798c2aab 100644 --- a/src/PhpDoc/Tag/DeprecatedTag.php +++ b/src/PhpDoc/Tag/DeprecatedTag.php @@ -6,11 +6,8 @@ class DeprecatedTag { - private ?string $message; - - public function __construct(?string $message) + public function __construct(private ?string $message) { - $this->message = $message; } public function getMessage(): ?string diff --git a/src/PhpDoc/Tag/ExtendsTag.php b/src/PhpDoc/Tag/ExtendsTag.php index 8e7330c273..74ed32b7a2 100644 --- a/src/PhpDoc/Tag/ExtendsTag.php +++ b/src/PhpDoc/Tag/ExtendsTag.php @@ -8,11 +8,8 @@ class ExtendsTag { - private \PHPStan\Type\Type $type; - - public function __construct(Type $type) + public function __construct(private Type $type) { - $this->type = $type; } public function getType(): Type diff --git a/src/PhpDoc/Tag/ImplementsTag.php b/src/PhpDoc/Tag/ImplementsTag.php index 17fcc7e310..cc9376b47a 100644 --- a/src/PhpDoc/Tag/ImplementsTag.php +++ b/src/PhpDoc/Tag/ImplementsTag.php @@ -8,11 +8,8 @@ class ImplementsTag { - private \PHPStan\Type\Type $type; - - public function __construct(Type $type) + public function __construct(private Type $type) { - $this->type = $type; } public function getType(): Type diff --git a/src/PhpDoc/Tag/MethodTag.php b/src/PhpDoc/Tag/MethodTag.php index 2f8ba3bea4..e640418ea8 100644 --- a/src/PhpDoc/Tag/MethodTag.php +++ b/src/PhpDoc/Tag/MethodTag.php @@ -8,27 +8,15 @@ class MethodTag { - private \PHPStan\Type\Type $returnType; - - private bool $isStatic; - - /** @var array */ - private array $parameters; - /** - * @param \PHPStan\Type\Type $returnType - * @param bool $isStatic - * @param array $parameters + * @param array $parameters */ public function __construct( - Type $returnType, - bool $isStatic, - array $parameters + private Type $returnType, + private bool $isStatic, + private array $parameters, ) { - $this->returnType = $returnType; - $this->isStatic = $isStatic; - $this->parameters = $parameters; } public function getReturnType(): Type @@ -42,7 +30,7 @@ public function isStatic(): bool } /** - * @return array + * @return array */ public function getParameters(): array { diff --git a/src/PhpDoc/Tag/MethodTagParameter.php b/src/PhpDoc/Tag/MethodTagParameter.php index 6f4043682e..1326c4cbc9 100644 --- a/src/PhpDoc/Tag/MethodTagParameter.php +++ b/src/PhpDoc/Tag/MethodTagParameter.php @@ -9,29 +9,14 @@ class MethodTagParameter { - private \PHPStan\Type\Type $type; - - private \PHPStan\Reflection\PassedByReference $passedByReference; - - private bool $isOptional; - - private bool $isVariadic; - - private ?\PHPStan\Type\Type $defaultValue; - public function __construct( - Type $type, - PassedByReference $passedByReference, - bool $isOptional, - bool $isVariadic, - ?Type $defaultValue + private Type $type, + private PassedByReference $passedByReference, + private bool $isOptional, + private bool $isVariadic, + private ?Type $defaultValue, ) { - $this->type = $type; - $this->passedByReference = $passedByReference; - $this->isOptional = $isOptional; - $this->isVariadic = $isVariadic; - $this->defaultValue = $defaultValue; } public function getType(): Type diff --git a/src/PhpDoc/Tag/MixinTag.php b/src/PhpDoc/Tag/MixinTag.php index 847af7b1d8..2a97b73264 100644 --- a/src/PhpDoc/Tag/MixinTag.php +++ b/src/PhpDoc/Tag/MixinTag.php @@ -8,11 +8,8 @@ class MixinTag { - private \PHPStan\Type\Type $type; - - public function __construct(Type $type) + public function __construct(private Type $type) { - $this->type = $type; } public function getType(): Type diff --git a/src/PhpDoc/Tag/ParamTag.php b/src/PhpDoc/Tag/ParamTag.php index 04d8fc76b0..651064cec1 100644 --- a/src/PhpDoc/Tag/ParamTag.php +++ b/src/PhpDoc/Tag/ParamTag.php @@ -8,14 +8,8 @@ class ParamTag implements TypedTag { - private \PHPStan\Type\Type $type; - - private bool $isVariadic; - - public function __construct(Type $type, bool $isVariadic) + public function __construct(private Type $type, private bool $isVariadic) { - $this->type = $type; - $this->isVariadic = $isVariadic; } public function getType(): Type @@ -29,7 +23,6 @@ public function isVariadic(): bool } /** - * @param Type $type * @return self */ public function withType(Type $type): TypedTag diff --git a/src/PhpDoc/Tag/PropertyTag.php b/src/PhpDoc/Tag/PropertyTag.php index 4b624dccf3..b204ce4bb3 100644 --- a/src/PhpDoc/Tag/PropertyTag.php +++ b/src/PhpDoc/Tag/PropertyTag.php @@ -8,21 +8,12 @@ class PropertyTag { - private \PHPStan\Type\Type $type; - - private bool $readable; - - private bool $writable; - public function __construct( - Type $type, - bool $readable, - bool $writable + private Type $type, + private bool $readable, + private bool $writable, ) { - $this->type = $type; - $this->readable = $readable; - $this->writable = $writable; } public function getType(): Type diff --git a/src/PhpDoc/Tag/ReturnTag.php b/src/PhpDoc/Tag/ReturnTag.php index aa75fcc5b2..683b0cb259 100644 --- a/src/PhpDoc/Tag/ReturnTag.php +++ b/src/PhpDoc/Tag/ReturnTag.php @@ -8,14 +8,8 @@ class ReturnTag implements TypedTag { - private \PHPStan\Type\Type $type; - - private bool $isExplicit; - - public function __construct(Type $type, bool $isExplicit) + public function __construct(private Type $type, private bool $isExplicit) { - $this->type = $type; - $this->isExplicit = $isExplicit; } public function getType(): Type @@ -29,7 +23,6 @@ public function isExplicit(): bool } /** - * @param Type $type * @return self */ public function withType(Type $type): TypedTag diff --git a/src/PhpDoc/Tag/TemplateTag.php b/src/PhpDoc/Tag/TemplateTag.php index b936b8c640..4a2180c052 100644 --- a/src/PhpDoc/Tag/TemplateTag.php +++ b/src/PhpDoc/Tag/TemplateTag.php @@ -9,17 +9,8 @@ class TemplateTag { - private string $name; - - private \PHPStan\Type\Type $bound; - - private TemplateTypeVariance $variance; - - public function __construct(string $name, Type $bound, TemplateTypeVariance $variance) + public function __construct(private string $name, private Type $bound, private TemplateTypeVariance $variance) { - $this->name = $name; - $this->bound = $bound; - $this->variance = $variance; } public function getName(): string diff --git a/src/PhpDoc/Tag/ThrowsTag.php b/src/PhpDoc/Tag/ThrowsTag.php index 1fbe1e2dc0..15c1ac94d9 100644 --- a/src/PhpDoc/Tag/ThrowsTag.php +++ b/src/PhpDoc/Tag/ThrowsTag.php @@ -8,11 +8,8 @@ class ThrowsTag { - private \PHPStan\Type\Type $type; - - public function __construct(Type $type) + public function __construct(private Type $type) { - $this->type = $type; } public function getType(): Type diff --git a/src/PhpDoc/Tag/TypeAliasImportTag.php b/src/PhpDoc/Tag/TypeAliasImportTag.php index 8edd46080f..ab074d6775 100644 --- a/src/PhpDoc/Tag/TypeAliasImportTag.php +++ b/src/PhpDoc/Tag/TypeAliasImportTag.php @@ -6,17 +6,8 @@ final class TypeAliasImportTag { - private string $importedAlias; - - private string $importedFrom; - - private ?string $importedAs; - - public function __construct(string $importedAlias, string $importedFrom, ?string $importedAs) + public function __construct(private string $importedAlias, private string $importedFrom, private ?string $importedAs) { - $this->importedAlias = $importedAlias; - $this->importedFrom = $importedFrom; - $this->importedAs = $importedAs; } public function getImportedAlias(): string diff --git a/src/PhpDoc/Tag/TypeAliasTag.php b/src/PhpDoc/Tag/TypeAliasTag.php index 8782dbd907..35b613417a 100644 --- a/src/PhpDoc/Tag/TypeAliasTag.php +++ b/src/PhpDoc/Tag/TypeAliasTag.php @@ -4,26 +4,18 @@ use PHPStan\Analyser\NameScope; use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Type\TypeAlias; /** @api */ class TypeAliasTag { - private string $aliasName; - - private TypeNode $typeNode; - - private NameScope $nameScope; - public function __construct( - string $aliasName, - TypeNode $typeNode, - NameScope $nameScope + private string $aliasName, + private TypeNode $typeNode, + private NameScope $nameScope, ) { - $this->aliasName = $aliasName; - $this->typeNode = $typeNode; - $this->nameScope = $nameScope; } public function getAliasName(): string @@ -31,11 +23,11 @@ public function getAliasName(): string return $this->aliasName; } - public function getTypeAlias(): \PHPStan\Type\TypeAlias + public function getTypeAlias(): TypeAlias { - return new \PHPStan\Type\TypeAlias( + return new TypeAlias( $this->typeNode, - $this->nameScope + $this->nameScope, ); } diff --git a/src/PhpDoc/Tag/TypedTag.php b/src/PhpDoc/Tag/TypedTag.php index 5d6f9c6bee..0be9218dce 100644 --- a/src/PhpDoc/Tag/TypedTag.php +++ b/src/PhpDoc/Tag/TypedTag.php @@ -11,7 +11,6 @@ interface TypedTag public function getType(): Type; /** - * @param Type $type * @return static */ public function withType(Type $type): self; diff --git a/src/PhpDoc/Tag/UsesTag.php b/src/PhpDoc/Tag/UsesTag.php index 950f58cba6..453eb5b250 100644 --- a/src/PhpDoc/Tag/UsesTag.php +++ b/src/PhpDoc/Tag/UsesTag.php @@ -8,11 +8,8 @@ class UsesTag { - private \PHPStan\Type\Type $type; - - public function __construct(Type $type) + public function __construct(private Type $type) { - $this->type = $type; } public function getType(): Type diff --git a/src/PhpDoc/Tag/VarTag.php b/src/PhpDoc/Tag/VarTag.php index 6c9a04d2ee..672cb81d43 100644 --- a/src/PhpDoc/Tag/VarTag.php +++ b/src/PhpDoc/Tag/VarTag.php @@ -8,11 +8,8 @@ class VarTag implements TypedTag { - private \PHPStan\Type\Type $type; - - public function __construct(Type $type) + public function __construct(private Type $type) { - $this->type = $type; } public function getType(): Type @@ -21,7 +18,6 @@ public function getType(): Type } /** - * @param Type $type * @return self */ public function withType(Type $type): TypedTag diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 918e4a8197..be7f04e1e4 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -2,9 +2,12 @@ namespace PHPStan\PhpDoc; +use Closure; +use Generator; +use Iterator; +use IteratorAggregate; use Nette\Utils\Strings; use PHPStan\Analyser\NameScope; -use PHPStan\DependencyInjection\Container; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFloatNode; @@ -28,6 +31,9 @@ use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\PassedByReference; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\ShouldNotHappenException; +use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; @@ -41,6 +47,7 @@ use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ConstantTypeHelper; +use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\ErrorType; use PHPStan\Type\FloatType; use PHPStan\Type\Generic\GenericClassStringType; @@ -61,29 +68,32 @@ use PHPStan\Type\ThisType; use PHPStan\Type\Type; use PHPStan\Type\TypeAliasResolver; +use PHPStan\Type\TypeAliasResolverProvider; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeUtils; use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; use PHPStan\Type\VoidType; +use Traversable; +use function array_map; +use function count; +use function get_class; +use function in_array; +use function preg_quote; +use function str_replace; +use function strpos; +use function strtolower; +use function substr; class TypeNodeResolver { - private TypeNodeResolverExtensionRegistryProvider $extensionRegistryProvider; - - private Container $container; - - private bool $deepInspectTypes; - public function __construct( - TypeNodeResolverExtensionRegistryProvider $extensionRegistryProvider, - Container $container, - bool $deepInspectTypes = false + private TypeNodeResolverExtensionRegistryProvider $extensionRegistryProvider, + private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, + private TypeAliasResolverProvider $typeAliasResolverProvider, ) { - $this->extensionRegistryProvider = $extensionRegistryProvider; - $this->container = $container; - $this->deepInspectTypes = $deepInspectTypes; } /** @api */ @@ -145,7 +155,12 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco case 'string': return new StringType(); + case 'literal-string': + return new IntersectionType([new StringType(), new AccessoryLiteralStringType()]); + case 'class-string': + case 'interface-string': + case 'trait-string': return new ClassStringType(); case 'callable-string': @@ -155,6 +170,12 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco return new BenevolentUnionType([new IntegerType(), new StringType()]); case 'scalar': + $type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope); + + if ($type !== null) { + return $type; + } + return new UnionType([new IntegerType(), new FloatType(), new StringType(), new BooleanType()]); case 'number': @@ -188,6 +209,12 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco new AccessoryNumericStringType(), ]); + case 'non-empty-string': + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + case 'bool': return new BooleanType(); @@ -228,7 +255,7 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco case 'non-empty-array': return TypeCombinator::intersect( new ArrayType(new MixedType(), new MixedType()), - new NonEmptyArrayType() + new NonEmptyArrayType(), ); case 'iterable': @@ -275,7 +302,7 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco case 'non-empty-list': return TypeCombinator::intersect( new ArrayType(new IntegerType(), new MixedType()), - new NonEmptyArrayType() + new NonEmptyArrayType(), ); } @@ -285,12 +312,17 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco return new ObjectType($nameScope->getClassName()); case 'static': - return new StaticType($nameScope->getClassName()); + if ($this->getReflectionProvider()->hasClass($nameScope->getClassName())) { + $classReflection = $this->getReflectionProvider()->getClass($nameScope->getClassName()); + + return new StaticType($classReflection); + } + return new ErrorType(); case 'parent': if ($this->getReflectionProvider()->hasClass($nameScope->getClassName())) { $classReflection = $this->getReflectionProvider()->getClass($nameScope->getClassName()); - if ($classReflection->getParentClass() !== false) { + if ($classReflection->getParentClass() !== null) { return new ObjectType($classReflection->getParentClass()->getName()); } } @@ -352,7 +384,7 @@ private function resolveThisTypeNode(ThisTypeNode $typeNode, NameScope $nameScop private function resolveNullableTypeNode(NullableTypeNode $typeNode, NameScope $nameScope): Type { - return TypeCombinator::addNull($this->resolve($typeNode->type, $nameScope)); + return TypeCombinator::union($this->resolve($typeNode->type, $nameScope), new NullType()); } private function resolveUnionTypeNode(UnionTypeNode $typeNode, NameScope $nameScope): Type @@ -409,7 +441,7 @@ private function resolveIntersectionTypeNode(IntersectionTypeNode $typeNode, Nam private function resolveArrayTypeNode(ArrayTypeNode $typeNode, NameScope $nameScope): Type { $itemType = $this->resolve($typeNode->type, $nameScope); - return new ArrayType(new MixedType(), $itemType); + return new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), $itemType); } private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $nameScope): Type @@ -419,16 +451,13 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na if ($mainTypeName === 'array' || $mainTypeName === 'non-empty-array') { if (count($genericTypes) === 1) { // array - $arrayType = new ArrayType(new MixedType(true), $genericTypes[0]); + $arrayType = new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), $genericTypes[0]); } elseif (count($genericTypes) === 2) { // array - $keyType = $genericTypes[0]; - if ($this->deepInspectTypes) { - $keyType = TypeCombinator::intersect($keyType, new UnionType([ - new IntegerType(), - new StringType(), - ])); - } - $arrayType = new ArrayType($keyType, $genericTypes[1]); + $keyType = TypeCombinator::intersect($genericTypes[0], new UnionType([ + new IntegerType(), + new StringType(), + ])); + $arrayType = new ArrayType(!$keyType instanceof NeverType ? ArrayType::castToArrayKeyType($keyType) : $keyType, $genericTypes[1]); } else { return new ErrorType(); } @@ -458,7 +487,7 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na if (count($genericTypes) === 2) { // iterable return new IterableType($genericTypes[0], $genericTypes[1]); } - } elseif ($mainTypeName === 'class-string') { + } elseif (in_array($mainTypeName, ['class-string', 'interface-string'], true)) { if (count($genericTypes) === 1) { $genericType = $genericTypes[0]; if ((new ObjectWithoutClassType())->isSuperTypeOf($genericType)->yes() || $genericType instanceof MixedType) { @@ -466,6 +495,44 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na } } + return new ErrorType(); + } elseif ($mainTypeName === 'int') { + if (count($genericTypes) === 2) { // int, int<1, 3> + + if ($genericTypes[0] instanceof ConstantIntegerType) { + $min = $genericTypes[0]->getValue(); + } elseif ($typeNode->genericTypes[0] instanceof IdentifierTypeNode && $typeNode->genericTypes[0]->name === 'min') { + $min = null; + } else { + return new ErrorType(); + } + + if ($genericTypes[1] instanceof ConstantIntegerType) { + $max = $genericTypes[1]->getValue(); + } elseif ($typeNode->genericTypes[1] instanceof IdentifierTypeNode && $typeNode->genericTypes[1]->name === 'max') { + $max = null; + } else { + return new ErrorType(); + } + + return IntegerRangeType::fromInterval($min, $max); + } + } elseif ($mainTypeName === 'key-of') { + if (count($genericTypes) === 1) { // key-of + return $genericTypes[0]->getIterableKeyType(); + } + + return new ErrorType(); + } elseif ($mainTypeName === 'value-of') { + if (count($genericTypes) === 1) { // value-of + return $genericTypes[0]->getIterableValueType(); + } + + return new ErrorType(); + } elseif ($mainTypeName === '__benevolent') { + if (count($genericTypes) === 1) { + return TypeUtils::toBenevolentUnion($genericTypes[0]); + } return new ErrorType(); } @@ -479,9 +546,9 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na $classReflection = $this->getReflectionProvider()->getClass($mainType->getClassName()); if ($classReflection->isGeneric()) { if (in_array($mainType->getClassName(), [ - \Traversable::class, - \IteratorAggregate::class, - \Iterator::class, + Traversable::class, + IteratorAggregate::class, + Iterator::class, ], true)) { if (count($genericTypes) === 1) { return new GenericObjectType($mainType->getClassName(), [ @@ -497,7 +564,7 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na ]); } } - if ($mainType->getClassName() === \Generator::class) { + if ($mainType->getClassName() === Generator::class) { if (count($genericTypes) === 1) { $mixed = new MixedType(true); return new GenericObjectType($mainType->getClassName(), [ @@ -536,14 +603,14 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na if (count($genericTypes) === 1) { // Foo return TypeCombinator::intersect( $mainType, - new IterableType(new MixedType(true), $genericTypes[0]) + new IterableType(new MixedType(true), $genericTypes[0]), ); } if (count($genericTypes) === 2) { // Foo return TypeCombinator::intersect( $mainType, - new IterableType($genericTypes[0], $genericTypes[1]) + new IterableType($genericTypes[0], $genericTypes[1]), ); } } @@ -572,10 +639,10 @@ function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadi $this->resolve($parameterNode->type, $nameScope), $parameterNode->isReference ? PassedByReference::createCreatesNewVariable() : PassedByReference::createNo(), $parameterNode->isVariadic, - null + null, ); }, - $typeNode->parameters + $typeNode->parameters, ); $returnType = $this->resolve($typeNode->returnType, $nameScope); @@ -584,7 +651,7 @@ function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadi } elseif ( $mainType instanceof ObjectType - && $mainType->getClassName() === \Closure::class + && $mainType->getClassName() === Closure::class ) { return new ClosureType($parameters, $returnType, $isVariadic); } @@ -605,7 +672,7 @@ private function resolveArrayShapeNode(ArrayShapeNode $typeNode, NameScope $name } elseif ($itemNode->keyName instanceof ConstExprStringNode) { $offsetType = new ConstantStringType($itemNode->keyName->value); } elseif ($itemNode->keyName !== null) { - throw new \PHPStan\ShouldNotHappenException('Unsupported key node type: ' . get_class($itemNode->keyName)); + throw new ShouldNotHappenException('Unsupported key node type: ' . get_class($itemNode->keyName)); } $builder->setOffsetValueType($offsetType, $this->resolve($itemNode->valueType, $nameScope), $itemNode->optional); } @@ -617,7 +684,7 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc { $constExpr = $typeNode->constExpr; if ($constExpr instanceof ConstExprArrayNode) { - throw new \PHPStan\ShouldNotHappenException(); // we prefer array shapes + throw new ShouldNotHappenException(); // we prefer array shapes } if ( @@ -625,12 +692,12 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc || $constExpr instanceof ConstExprTrueNode || $constExpr instanceof ConstExprNullNode ) { - throw new \PHPStan\ShouldNotHappenException(); // we prefer IdentifierTypeNode + throw new ShouldNotHappenException(); // we prefer IdentifierTypeNode } if ($constExpr instanceof ConstFetchNode) { if ($constExpr->className === '') { - throw new \PHPStan\ShouldNotHappenException(); // global constant should get parsed as class name in IdentifierTypeNode + throw new ShouldNotHappenException(); // global constant should get parsed as class name in IdentifierTypeNode } if ($nameScope->getClassName() !== null) { @@ -643,7 +710,7 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc case 'parent': if ($this->getReflectionProvider()->hasClass($nameScope->getClassName())) { $classReflection = $this->getReflectionProvider()->getClass($nameScope->getClassName()); - if ($classReflection->getParentClass() === false) { + if ($classReflection->getParentClass() === null) { return new ErrorType(); } @@ -664,11 +731,17 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc $classReflection = $this->getReflectionProvider()->getClass($className); $constantName = $constExpr->name; - if (Strings::endsWith($constantName, '*')) { - $constantNameStartsWith = Strings::substring($constantName, 0, Strings::length($constantName) - 1); + if (Strings::contains($constantName, '*')) { + // convert * into .*? and escape everything else so the constants can be matched against the pattern + $pattern = '{^' . str_replace('\\*', '.*?', preg_quote($constantName)) . '$}D'; $constantTypes = []; foreach ($classReflection->getNativeReflection()->getConstants() as $classConstantName => $constantValue) { - if (!Strings::startsWith($classConstantName, $constantNameStartsWith)) { + if (Strings::match($classConstantName, $pattern) === null) { + continue; + } + + if ($classReflection->isEnum() && $classReflection->hasEnumCase($classConstantName)) { + $constantTypes[] = new EnumCaseObjectType($classReflection->getName(), $classConstantName); continue; } @@ -686,7 +759,11 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc return new ErrorType(); } - return $classReflection->getConstant($constantName)->getValueType(); + if ($classReflection->isEnum() && $classReflection->hasEnumCase($constantName)) { + return new EnumCaseObjectType($classReflection->getName(), $constantName); + } + + return ConstantTypeHelper::getTypeFromValue($classReflection->getConstant($constantName)->getValue()); } if ($constExpr instanceof ConstExprFloatNode) { @@ -707,7 +784,6 @@ private function resolveConstTypeNode(ConstTypeNode $typeNode, NameScope $nameSc /** * @api * @param TypeNode[] $typeNodes - * @param NameScope $nameScope * @return Type[] */ public function resolveMultiple(array $typeNodes, NameScope $nameScope): array @@ -722,12 +798,12 @@ public function resolveMultiple(array $typeNodes, NameScope $nameScope): array private function getReflectionProvider(): ReflectionProvider { - return $this->container->getByType(ReflectionProvider::class); + return $this->reflectionProviderProvider->getReflectionProvider(); } private function getTypeAliasResolver(): TypeAliasResolver { - return $this->container->getByType(TypeAliasResolver::class); + return $this->typeAliasResolverProvider->getTypeAliasResolver(); } } diff --git a/src/PhpDoc/TypeNodeResolverExtensionAwareRegistry.php b/src/PhpDoc/TypeNodeResolverExtensionAwareRegistry.php new file mode 100644 index 0000000000..a41b9c631c --- /dev/null +++ b/src/PhpDoc/TypeNodeResolverExtensionAwareRegistry.php @@ -0,0 +1,33 @@ +setTypeNodeResolver($typeNodeResolver); + } + } + + /** + * @return TypeNodeResolverExtension[] + */ + public function getExtensions(): array + { + return $this->extensions; + } + +} diff --git a/src/PhpDoc/TypeNodeResolverExtensionRegistry.php b/src/PhpDoc/TypeNodeResolverExtensionRegistry.php index 8b74ad1e50..93883a32ea 100644 --- a/src/PhpDoc/TypeNodeResolverExtensionRegistry.php +++ b/src/PhpDoc/TypeNodeResolverExtensionRegistry.php @@ -2,36 +2,12 @@ namespace PHPStan\PhpDoc; -class TypeNodeResolverExtensionRegistry +interface TypeNodeResolverExtensionRegistry { - /** @var TypeNodeResolverExtension[] */ - private array $extensions; - - /** - * @param TypeNodeResolverExtension[] $extensions - */ - public function __construct( - TypeNodeResolver $typeNodeResolver, - array $extensions - ) - { - foreach ($extensions as $extension) { - if (!$extension instanceof TypeNodeResolverAwareExtension) { - continue; - } - - $extension->setTypeNodeResolver($typeNodeResolver); - } - $this->extensions = $extensions; - } - /** * @return TypeNodeResolverExtension[] */ - public function getExtensions(): array - { - return $this->extensions; - } + public function getExtensions(): array; } diff --git a/src/PhpDoc/TypeStringResolver.php b/src/PhpDoc/TypeStringResolver.php index 91e8ec4df0..3933e1d25b 100644 --- a/src/PhpDoc/TypeStringResolver.php +++ b/src/PhpDoc/TypeStringResolver.php @@ -11,19 +11,11 @@ class TypeStringResolver { - private Lexer $typeLexer; - - private TypeParser $typeParser; - - private TypeNodeResolver $typeNodeResolver; - - public function __construct(Lexer $typeLexer, TypeParser $typeParser, TypeNodeResolver $typeNodeResolver) + public function __construct(private Lexer $typeLexer, private TypeParser $typeParser, private TypeNodeResolver $typeNodeResolver) { - $this->typeLexer = $typeLexer; - $this->typeParser = $typeParser; - $this->typeNodeResolver = $typeNodeResolver; } + /** @api */ public function resolve(string $typeString, ?NameScope $nameScope = null): Type { $tokens = new TokenIterator($this->typeLexer->tokenize($typeString)); diff --git a/src/Process/CpuCoreCounter.php b/src/Process/CpuCoreCounter.php index 2bc1b2b0a2..609baad081 100644 --- a/src/Process/CpuCoreCounter.php +++ b/src/Process/CpuCoreCounter.php @@ -2,6 +2,17 @@ namespace PHPStan\Process; +use function count; +use function fgets; +use function file_get_contents; +use function function_exists; +use function is_file; +use function is_resource; +use function pclose; +use function popen; +use function preg_match_all; +use const DIRECTORY_SEPARATOR; + class CpuCoreCounter { @@ -27,7 +38,7 @@ public function getNumberOfCpuCores(): int } } - if (\DIRECTORY_SEPARATOR === '\\') { + if (DIRECTORY_SEPARATOR === '\\') { // Windows $process = @popen('wmic cpu get NumberOfLogicalProcessors', 'rb'); if (is_resource($process)) { @@ -39,7 +50,7 @@ public function getNumberOfCpuCores(): int } } - $process = @\popen('sysctl -n hw.ncpu', 'rb'); + $process = @popen('sysctl -n hw.ncpu', 'rb'); if (is_resource($process)) { // *nix (Linux, BSD and Mac) $cores = (int) fgets($process); diff --git a/src/Process/ProcessCanceledException.php b/src/Process/ProcessCanceledException.php index fb1e5775d8..9d37c5b6ca 100644 --- a/src/Process/ProcessCanceledException.php +++ b/src/Process/ProcessCanceledException.php @@ -2,7 +2,9 @@ namespace PHPStan\Process; -class ProcessCanceledException extends \Exception +use Exception; + +class ProcessCanceledException extends Exception { } diff --git a/src/Process/ProcessCrashedException.php b/src/Process/ProcessCrashedException.php index 8ecd33c4b7..d6278a50e7 100644 --- a/src/Process/ProcessCrashedException.php +++ b/src/Process/ProcessCrashedException.php @@ -2,7 +2,9 @@ namespace PHPStan\Process; -class ProcessCrashedException extends \Exception +use Exception; + +class ProcessCrashedException extends Exception { } diff --git a/src/Process/ProcessHelper.php b/src/Process/ProcessHelper.php index c3d6a7d05e..2597e771a8 100644 --- a/src/Process/ProcessHelper.php +++ b/src/Process/ProcessHelper.php @@ -4,24 +4,27 @@ use PHPStan\Command\AnalyseCommand; use Symfony\Component\Console\Input\InputInterface; +use function array_merge; +use function escapeshellarg; +use function implode; +use function ini_get; +use function is_bool; +use function php_ini_loaded_file; +use function sprintf; +use const PHP_BINARY; class ProcessHelper { /** - * @param string $mainScript - * @param string $commandName - * @param string|null $projectConfigFile * @param string[] $additionalItems - * @param InputInterface $input - * @return string */ public static function getWorkerCommand( string $mainScript, string $commandName, ?string $projectConfigFile, array $additionalItems, - InputInterface $input + InputInterface $input, ): string { $phpIni = php_ini_loaded_file(); @@ -46,11 +49,11 @@ public static function getWorkerCommand( } $options = [ - 'paths-file', AnalyseCommand::OPTION_LEVEL, 'autoload-file', 'memory-limit', 'xdebug', + 'verbose', ]; foreach ($options as $optionName) { /** @var bool|string|null $optionValue */ diff --git a/src/Process/ProcessPromise.php b/src/Process/ProcessPromise.php index 8441d82427..26b7fb256a 100644 --- a/src/Process/ProcessPromise.php +++ b/src/Process/ProcessPromise.php @@ -3,35 +3,28 @@ namespace PHPStan\Process; use PHPStan\Process\Runnable\Runnable; +use PHPStan\ShouldNotHappenException; use React\ChildProcess\Process; use React\EventLoop\LoopInterface; use React\Promise\CancellablePromiseInterface; use React\Promise\Deferred; use React\Promise\ExtendedPromiseInterface; +use function fclose; +use function rewind; +use function stream_get_contents; +use function tmpfile; class ProcessPromise implements Runnable { - /** @var LoopInterface */ - private $loop; - - /** @var string */ - private $name; - - /** @var string */ - private $command; - private Deferred $deferred; private ?Process $process = null; private bool $canceled = false; - public function __construct(LoopInterface $loop, string $name, string $command) + public function __construct(private LoopInterface $loop, private string $name, private string $command) { - $this->loop = $loop; - $this->name = $name; - $this->command = $command; $this->deferred = new Deferred(); } @@ -47,11 +40,11 @@ public function run(): CancellablePromiseInterface { $tmpStdOutResource = tmpfile(); if ($tmpStdOutResource === false) { - throw new \PHPStan\ShouldNotHappenException('Failed creating temp file for stdout.'); + throw new ShouldNotHappenException('Failed creating temp file for stdout.'); } $tmpStdErrResource = tmpfile(); if ($tmpStdErrResource === false) { - throw new \PHPStan\ShouldNotHappenException('Failed creating temp file for stderr.'); + throw new ShouldNotHappenException('Failed creating temp file for stderr.'); } $this->process = new Process($this->command, null, null, [ @@ -75,7 +68,7 @@ public function run(): CancellablePromiseInterface fclose($tmpStdErrResource); if ($exitCode === null) { - $this->deferred->reject(new \PHPStan\Process\ProcessCrashedException($stdOut . $stdErr)); + $this->deferred->reject(new ProcessCrashedException($stdOut . $stdErr)); return; } @@ -84,7 +77,7 @@ public function run(): CancellablePromiseInterface return; } - $this->deferred->reject(new \PHPStan\Process\ProcessCrashedException($stdOut . $stdErr)); + $this->deferred->reject(new ProcessCrashedException($stdOut . $stdErr)); }); /** @var ExtendedPromiseInterface&CancellablePromiseInterface */ @@ -94,11 +87,11 @@ public function run(): CancellablePromiseInterface public function cancel(): void { if ($this->process === null) { - throw new \PHPStan\ShouldNotHappenException('Cancelling process before running'); + throw new ShouldNotHappenException('Cancelling process before running'); } $this->canceled = true; $this->process->terminate(); - $this->deferred->reject(new \PHPStan\Process\ProcessCanceledException()); + $this->deferred->reject(new ProcessCanceledException()); } } diff --git a/src/Process/Runnable/RunnableCanceledException.php b/src/Process/Runnable/RunnableCanceledException.php index 2241ae151b..2a002fd9a9 100644 --- a/src/Process/Runnable/RunnableCanceledException.php +++ b/src/Process/Runnable/RunnableCanceledException.php @@ -2,7 +2,9 @@ namespace PHPStan\Process\Runnable; -class RunnableCanceledException extends \Exception +use Exception; + +class RunnableCanceledException extends Exception { } diff --git a/src/Process/Runnable/RunnableQueue.php b/src/Process/Runnable/RunnableQueue.php index 10e98f50c4..50cccba972 100644 --- a/src/Process/Runnable/RunnableQueue.php +++ b/src/Process/Runnable/RunnableQueue.php @@ -2,28 +2,26 @@ namespace PHPStan\Process\Runnable; +use PHPStan\ShouldNotHappenException; use React\Promise\CancellablePromiseInterface; use React\Promise\Deferred; use SplObjectStorage; +use Throwable; +use function array_shift; +use function count; +use function sprintf; class RunnableQueue { - private RunnableQueueLogger $logger; - - private int $maxSize; - /** @var array */ private array $queue = []; /** @var SplObjectStorage */ private SplObjectStorage $running; - public function __construct(RunnableQueueLogger $logger, int $maxSize) + public function __construct(private RunnableQueueLogger $logger, private int $maxSize) { - $this->logger = $logger; - $this->maxSize = $maxSize; - /** @var SplObjectStorage $running */ $running = new SplObjectStorage(); $this->running = $running; @@ -53,7 +51,7 @@ public function getRunningSize(): int public function queue(Runnable $runnable, int $size): CancellablePromiseInterface { if ($size > $this->maxSize) { - throw new \PHPStan\ShouldNotHappenException('Runnable size exceeds queue maxSize.'); + throw new ShouldNotHappenException('Runnable size exceeds queue maxSize.'); } $deferred = new Deferred(static function () use ($runnable): void { @@ -75,7 +73,7 @@ private function drainQueue(): void $currentQueueSize = $this->getRunningSize(); if ($currentQueueSize > $this->maxSize) { - throw new \PHPStan\ShouldNotHappenException('Running overflow'); + throw new ShouldNotHappenException('Running overflow'); } if ($currentQueueSize === $this->maxSize) { @@ -94,8 +92,8 @@ private function drainQueue(): void 'Canot remote first item from the queue - it has size %d, current queue size is %d, new size would be %d', $runnableSize, $currentQueueSize, - $newSize - ) + $newSize, + ), ); return; } @@ -105,7 +103,7 @@ private function drainQueue(): void /** @var array{Runnable, int, Deferred} $popped */ $popped = array_shift($this->queue); if ($popped[0] !== $runnable || $popped[1] !== $runnableSize || $popped[2] !== $deferred) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $this->running->attach($runnable, [$runnableSize, $deferred]); @@ -115,7 +113,7 @@ private function drainQueue(): void $deferred->resolve($value); $this->running->detach($runnable); $this->drainQueue(); - }, function (\Throwable $e) use ($runnable, $deferred): void { + }, 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); diff --git a/src/Reflection/Annotations/AnnotationMethodReflection.php b/src/Reflection/Annotations/AnnotationMethodReflection.php index c63f2c4108..d33665d4b8 100644 --- a/src/Reflection/Annotations/AnnotationMethodReflection.php +++ b/src/Reflection/Annotations/AnnotationMethodReflection.php @@ -6,6 +6,7 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\FunctionVariant; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParametersAcceptor; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Type; @@ -13,45 +14,21 @@ class AnnotationMethodReflection implements MethodReflection { - private string $name; - - private \PHPStan\Reflection\ClassReflection $declaringClass; - - private Type $returnType; - - private bool $isStatic; - - /** @var \PHPStan\Reflection\Annotations\AnnotationsMethodParameterReflection[] */ - private array $parameters; - - private bool $isVariadic; - /** @var FunctionVariant[]|null */ private ?array $variants = null; /** - * @param string $name - * @param ClassReflection $declaringClass - * @param Type $returnType - * @param \PHPStan\Reflection\Annotations\AnnotationsMethodParameterReflection[] $parameters - * @param bool $isStatic - * @param bool $isVariadic + * @param AnnotationsMethodParameterReflection[] $parameters */ public function __construct( - string $name, - ClassReflection $declaringClass, - Type $returnType, - array $parameters, - bool $isStatic, - bool $isVariadic + private string $name, + private ClassReflection $declaringClass, + private Type $returnType, + private array $parameters, + private bool $isStatic, + private bool $isVariadic, ) { - $this->name = $name; - $this->declaringClass = $declaringClass; - $this->returnType = $returnType; - $this->parameters = $parameters; - $this->isStatic = $isStatic; - $this->isVariadic = $isVariadic; } public function getDeclaringClass(): ClassReflection @@ -85,7 +62,7 @@ public function getName(): string } /** - * @return \PHPStan\Reflection\ParametersAcceptor[] + * @return ParametersAcceptor[] */ public function getVariants(): array { @@ -96,7 +73,7 @@ public function getVariants(): array null, $this->parameters, $this->isVariadic, - $this->returnType + $this->returnType, ), ]; } diff --git a/src/Reflection/Annotations/AnnotationPropertyReflection.php b/src/Reflection/Annotations/AnnotationPropertyReflection.php index acbaaaac09..3613e87707 100644 --- a/src/Reflection/Annotations/AnnotationPropertyReflection.php +++ b/src/Reflection/Annotations/AnnotationPropertyReflection.php @@ -10,25 +10,13 @@ class AnnotationPropertyReflection implements PropertyReflection { - private \PHPStan\Reflection\ClassReflection $declaringClass; - - private \PHPStan\Type\Type $type; - - private bool $readable; - - private bool $writable; - public function __construct( - ClassReflection $declaringClass, - Type $type, - bool $readable = true, - bool $writable = true + private ClassReflection $declaringClass, + private Type $type, + private bool $readable = true, + private bool $writable = true, ) { - $this->declaringClass = $declaringClass; - $this->type = $type; - $this->readable = $readable; - $this->writable = $writable; } public function getDeclaringClass(): ClassReflection diff --git a/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php b/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php index 40d9c42981..8b51b325cf 100644 --- a/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php +++ b/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php @@ -9,26 +9,8 @@ class AnnotationsMethodParameterReflection implements ParameterReflection { - private string $name; - - private Type $type; - - private \PHPStan\Reflection\PassedByReference $passedByReference; - - private bool $isOptional; - - private bool $isVariadic; - - private ?Type $defaultValue; - - public function __construct(string $name, Type $type, PassedByReference $passedByReference, bool $isOptional, bool $isVariadic, ?Type $defaultValue) + public function __construct(private string $name, private Type $type, private PassedByReference $passedByReference, private bool $isOptional, private bool $isVariadic, private ?Type $defaultValue) { - $this->name = $name; - $this->type = $type; - $this->passedByReference = $passedByReference; - $this->isOptional = $isOptional; - $this->isVariadic = $isVariadic; - $this->defaultValue = $defaultValue; } public function getName(): string diff --git a/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php b/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php index 40271a382b..a51ab77b65 100644 --- a/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php +++ b/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php @@ -6,6 +6,7 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\MethodsClassReflectionExtension; use PHPStan\Type\Generic\TemplateTypeHelper; +use function count; class AnnotationsMethodsClassReflectionExtension implements MethodsClassReflectionExtension { @@ -34,7 +35,7 @@ public function getMethod(ClassReflection $classReflection, string $methodName): private function findClassReflectionWithMethod( ClassReflection $classReflection, ClassReflection $declaringClass, - string $methodName + string $methodName, ): ?MethodReflection { $methodTags = $classReflection->getMethodTags(); @@ -47,7 +48,7 @@ private function findClassReflectionWithMethod( $parameterTag->passedByReference(), $parameterTag->isOptional(), $parameterTag->isVariadic(), - $parameterTag->getDefaultValue() + $parameterTag->getDefaultValue(), ); } @@ -56,11 +57,11 @@ private function findClassReflectionWithMethod( $declaringClass, TemplateTypeHelper::resolveTemplateTypes( $methodTags[$methodName]->getReturnType(), - $classReflection->getActiveTemplateTypeMap() + $classReflection->getActiveTemplateTypeMap(), ), $parameters, $methodTags[$methodName]->isStatic(), - $this->detectMethodVariadic($parameters) + $this->detectMethodVariadic($parameters), ); } @@ -104,7 +105,6 @@ private function findClassReflectionWithMethod( /** * @param AnnotationsMethodParameterReflection[] $parameters - * @return bool */ private function detectMethodVariadic(array $parameters): bool { diff --git a/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php b/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php index f48da8542e..324f14b2ab 100644 --- a/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php +++ b/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php @@ -34,7 +34,7 @@ public function getProperty(ClassReflection $classReflection, string $propertyNa private function findClassReflectionWithProperty( ClassReflection $classReflection, ClassReflection $declaringClass, - string $propertyName + string $propertyName, ): ?PropertyReflection { $propertyTags = $classReflection->getPropertyTags(); @@ -43,10 +43,10 @@ private function findClassReflectionWithProperty( $declaringClass, TemplateTypeHelper::resolveTemplateTypes( $propertyTags[$propertyName]->getType(), - $classReflection->getActiveTemplateTypeMap() + $classReflection->getActiveTemplateTypeMap(), ), $propertyTags[$propertyName]->isReadable(), - $propertyTags[$propertyName]->isWritable() + $propertyTags[$propertyName]->isWritable(), ); } diff --git a/src/Reflection/BetterReflection/BetterReflectionProvider.php b/src/Reflection/BetterReflection/BetterReflectionProvider.php index 35cf41a275..7a3a5abd3b 100644 --- a/src/Reflection/BetterReflection/BetterReflectionProvider.php +++ b/src/Reflection/BetterReflection/BetterReflectionProvider.php @@ -2,6 +2,8 @@ namespace PHPStan\Reflection\BetterReflection; +use Closure; +use PhpParser\Node; use PhpParser\PrettyPrinter\Standard; use PHPStan\Analyser\Scope; use PHPStan\BetterReflection\Identifier\Exception\InvalidIdentifierName; @@ -10,17 +12,20 @@ use PHPStan\BetterReflection\Reflection\Adapter\ReflectionFunction; use PHPStan\BetterReflection\Reflection\Exception\NotAClassReflection; use PHPStan\BetterReflection\Reflection\Exception\NotAnInterfaceReflection; -use PHPStan\BetterReflection\Reflector\ClassReflector; -use PHPStan\BetterReflection\Reflector\ConstantReflector; +use PHPStan\BetterReflection\Reflection\ReflectionEnum; use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; -use PHPStan\BetterReflection\Reflector\FunctionReflector; +use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource; use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; use PHPStan\Broker\AnonymousClassNameHelper; +use PHPStan\Broker\ClassNotFoundException; +use PHPStan\Broker\ConstantNotFoundException; +use PHPStan\Broker\FunctionNotFoundException; use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider; use PHPStan\File\FileHelper; use PHPStan\File\RelativePathHelper; use PHPStan\Php\PhpVersion; +use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\PhpDoc\StubPhpDocProvider; use PHPStan\PhpDoc\Tag\ParamTag; use PHPStan\Reflection\ClassNameHelper; @@ -29,89 +34,55 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\FunctionReflectionFactory; use PHPStan\Reflection\GlobalConstantReflection; +use PHPStan\Reflection\Php\PhpFunctionReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\SignatureMap\NativeFunctionReflectionProvider; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\ConstantTypeHelper; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\MixedType; use PHPStan\Type\Type; +use ReflectionParameter; +use function array_key_exists; +use function array_map; +use function base64_decode; +use function sprintf; +use function strtolower; +use const PHP_VERSION_ID; class BetterReflectionProvider implements ReflectionProvider { - private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider; - - private \PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider; - - private \PHPStan\BetterReflection\Reflector\ClassReflector $classReflector; - - private \PHPStan\BetterReflection\Reflector\FunctionReflector $functionReflector; - - private \PHPStan\BetterReflection\Reflector\ConstantReflector $constantReflector; - - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - private PhpVersion $phpVersion; - - private \PHPStan\Reflection\SignatureMap\NativeFunctionReflectionProvider $nativeFunctionReflectionProvider; - - private StubPhpDocProvider $stubPhpDocProvider; - - private \PHPStan\Reflection\FunctionReflectionFactory $functionReflectionFactory; - - private RelativePathHelper $relativePathHelper; - - private AnonymousClassNameHelper $anonymousClassNameHelper; - - private \PhpParser\PrettyPrinter\Standard $printer; - - private \PHPStan\File\FileHelper $fileHelper; - - private PhpStormStubsSourceStubber $phpstormStubsSourceStubber; - - /** @var \PHPStan\Reflection\FunctionReflection[] */ + /** @var FunctionReflection[] */ private array $functionReflections = []; - /** @var \PHPStan\Reflection\ClassReflection[] */ + /** @var ClassReflection[] */ private array $classReflections = []; - /** @var \PHPStan\Reflection\ClassReflection[] */ + /** @var ClassReflection[] */ private static array $anonymousClasses = []; + /** @var array */ + private array $cachedConstants = []; + public function __construct( - ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, - ClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider, - ClassReflector $classReflector, - FileTypeMapper $fileTypeMapper, - PhpVersion $phpVersion, - NativeFunctionReflectionProvider $nativeFunctionReflectionProvider, - StubPhpDocProvider $stubPhpDocProvider, - FunctionReflectionFactory $functionReflectionFactory, - RelativePathHelper $relativePathHelper, - AnonymousClassNameHelper $anonymousClassNameHelper, - Standard $printer, - FileHelper $fileHelper, - FunctionReflector $functionReflector, - ConstantReflector $constantReflector, - PhpStormStubsSourceStubber $phpstormStubsSourceStubber + private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, + private ClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider, + private Reflector $reflector, + private FileTypeMapper $fileTypeMapper, + private PhpDocInheritanceResolver $phpDocInheritanceResolver, + private PhpVersion $phpVersion, + private NativeFunctionReflectionProvider $nativeFunctionReflectionProvider, + private StubPhpDocProvider $stubPhpDocProvider, + private FunctionReflectionFactory $functionReflectionFactory, + private RelativePathHelper $relativePathHelper, + private AnonymousClassNameHelper $anonymousClassNameHelper, + private Standard $printer, + private FileHelper $fileHelper, + private PhpStormStubsSourceStubber $phpstormStubsSourceStubber, ) { - $this->reflectionProviderProvider = $reflectionProviderProvider; - $this->classReflectionExtensionRegistryProvider = $classReflectionExtensionRegistryProvider; - $this->classReflector = $classReflector; - $this->fileTypeMapper = $fileTypeMapper; - $this->phpVersion = $phpVersion; - $this->nativeFunctionReflectionProvider = $nativeFunctionReflectionProvider; - $this->stubPhpDocProvider = $stubPhpDocProvider; - $this->functionReflectionFactory = $functionReflectionFactory; - $this->relativePathHelper = $relativePathHelper; - $this->anonymousClassNameHelper = $anonymousClassNameHelper; - $this->printer = $printer; - $this->fileHelper = $fileHelper; - $this->functionReflector = $functionReflector; - $this->constantReflector = $constantReflector; - $this->phpstormStubsSourceStubber = $phpstormStubsSourceStubber; } public function hasClass(string $className): bool @@ -125,11 +96,11 @@ public function hasClass(string $className): bool } try { - $this->classReflector->reflect($className); + $this->reflector->reflectClass($className); return true; - } catch (IdentifierNotFound $e) { + } catch (IdentifierNotFound) { return false; - } catch (InvalidIdentifierName $e) { + } catch (InvalidIdentifierName) { return false; } } @@ -141,9 +112,9 @@ public function getClass(string $className): ClassReflection } try { - $reflectionClass = $this->classReflector->reflect($className); - } catch (IdentifierNotFound $e) { - throw new \PHPStan\Broker\ClassNotFoundException($className); + $reflectionClass = $this->reflector->reflectClass($className); + } catch (IdentifierNotFound) { + throw new ClassNotFoundException($className); } $reflectionClassName = strtolower($reflectionClass->getName()); @@ -152,17 +123,21 @@ public function getClass(string $className): ClassReflection return $this->classReflections[$reflectionClassName]; } + $enumAdapter = base64_decode('UEhQU3RhblxCZXR0ZXJSZWZsZWN0aW9uXFJlZmxlY3Rpb25cQWRhcHRlclxSZWZsZWN0aW9uRW51bQ==', true); + $classReflection = new ClassReflection( $this->reflectionProviderProvider->getReflectionProvider(), $this->fileTypeMapper, + $this->stubPhpDocProvider, + $this->phpDocInheritanceResolver, $this->phpVersion, $this->classReflectionExtensionRegistryProvider->getRegistry()->getPropertiesClassReflectionExtensions(), $this->classReflectionExtensionRegistryProvider->getRegistry()->getMethodsClassReflectionExtensions(), $reflectionClass->getName(), - new ReflectionClass($reflectionClass), + $reflectionClass instanceof ReflectionEnum && PHP_VERSION_ID >= 80000 ? new $enumAdapter($reflectionClass) : new ReflectionClass($reflectionClass), null, null, - $this->stubPhpDocProvider->findClassPhpDoc($reflectionClass->getName()) + $this->stubPhpDocProvider->findClassPhpDoc($reflectionClass->getName()), ); $this->classReflections[$reflectionClassName] = $classReflection; @@ -173,14 +148,14 @@ public function getClass(string $className): ClassReflection public function getClassName(string $className): string { if (!$this->hasClass($className)) { - throw new \PHPStan\Broker\ClassNotFoundException($className); + throw new ClassNotFoundException($className); } if (isset(self::$anonymousClasses[$className])) { return self::$anonymousClasses[$className]->getDisplayName(); } - $reflectionClass = $this->classReflector->reflect($className); + $reflectionClass = $this->reflector->reflectClass($className); return $reflectionClass->getName(); } @@ -190,17 +165,17 @@ public function supportsAnonymousClasses(): bool return true; } - public function getAnonymousClassReflection(\PhpParser\Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection + public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection { if (isset($classNode->namespacedName)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if (!$scope->isInTrait()) { $scopeFile = $scope->getFile(); } else { $scopeFile = $scope->getTraitReflection()->getFileName(); - if ($scopeFile === false) { + if ($scopeFile === null) { $scopeFile = $scope->getFile(); } } @@ -208,9 +183,9 @@ public function getAnonymousClassReflection(\PhpParser\Node\Stmt\Class_ $classNo $filename = $this->fileHelper->normalizePath($this->relativePathHelper->getRelativePath($scopeFile), '/'); $className = $this->anonymousClassNameHelper->getAnonymousClassName( $classNode, - $scopeFile + $scopeFile, ); - $classNode->name = new \PhpParser\Node\Identifier($className); + $classNode->name = new Node\Identifier($className); $classNode->setAttribute('anonymousClass', true); if (isset(self::$anonymousClasses[$className])) { @@ -218,15 +193,17 @@ public function getAnonymousClassReflection(\PhpParser\Node\Stmt\Class_ $classNo } $reflectionClass = \PHPStan\BetterReflection\Reflection\ReflectionClass::createFromNode( - $this->classReflector, + $this->reflector, $classNode, - new LocatedSource($this->printer->prettyPrint([$classNode]), $scopeFile), - null + new LocatedSource($this->printer->prettyPrint([$classNode]), $className, $scopeFile), + null, ); self::$anonymousClasses[$className] = new ClassReflection( $this->reflectionProviderProvider->getReflectionProvider(), $this->fileTypeMapper, + $this->stubPhpDocProvider, + $this->phpDocInheritanceResolver, $this->phpVersion, $this->classReflectionExtensionRegistryProvider->getRegistry()->getPropertiesClassReflectionExtensions(), $this->classReflectionExtensionRegistryProvider->getRegistry()->getMethodsClassReflectionExtensions(), @@ -234,23 +211,23 @@ public function getAnonymousClassReflection(\PhpParser\Node\Stmt\Class_ $classNo new ReflectionClass($reflectionClass), $scopeFile, null, - $this->stubPhpDocProvider->findClassPhpDoc($className) + $this->stubPhpDocProvider->findClassPhpDoc($className), ); $this->classReflections[$className] = self::$anonymousClasses[$className]; return self::$anonymousClasses[$className]; } - public function hasFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool + public function hasFunction(Node\Name $nameNode, ?Scope $scope): bool { return $this->resolveFunctionName($nameNode, $scope) !== null; } - public function getFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): FunctionReflection + public function getFunction(Node\Name $nameNode, ?Scope $scope): FunctionReflection { $functionName = $this->resolveFunctionName($nameNode, $scope); if ($functionName === null) { - throw new \PHPStan\Broker\FunctionNotFoundException((string) $nameNode); + throw new FunctionNotFoundException((string) $nameNode); } $lowerCasedFunctionName = strtolower($functionName); @@ -269,9 +246,9 @@ public function getFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): Func return $this->functionReflections[$lowerCasedFunctionName]; } - private function getCustomFunction(string $functionName): \PHPStan\Reflection\Php\PhpFunctionReflection + private function getCustomFunction(string $functionName): PhpFunctionReflection { - $reflectionFunction = new ReflectionFunction($this->functionReflector->reflect($functionName)); + $reflectionFunction = new ReflectionFunction($this->reflector->reflectFunction($functionName)); $templateTypeMap = TemplateTypeMap::createEmpty(); $phpDocParameterTags = []; $phpDocReturnTag = null; @@ -281,13 +258,10 @@ private function getCustomFunction(string $functionName): \PHPStan\Reflection\Ph $isInternal = false; $isFinal = false; $isPure = null; - $resolvedPhpDoc = $this->stubPhpDocProvider->findFunctionPhpDoc($reflectionFunction->getName(), array_map(static function (\ReflectionParameter $parameter): string { - return $parameter->getName(); - }, $reflectionFunction->getParameters())); + $resolvedPhpDoc = $this->stubPhpDocProvider->findFunctionPhpDoc($reflectionFunction->getName(), array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $reflectionFunction->getParameters())); if ($resolvedPhpDoc === null && $reflectionFunction->getFileName() !== false && $reflectionFunction->getDocComment() !== false) { - $fileName = $reflectionFunction->getFileName(); $docComment = $reflectionFunction->getDocComment(); - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc($fileName, null, null, $reflectionFunction->getName(), $docComment); + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc($reflectionFunction->getFileName(), null, null, $reflectionFunction->getName(), $docComment); } if ($resolvedPhpDoc !== null) { @@ -305,29 +279,27 @@ private function getCustomFunction(string $functionName): \PHPStan\Reflection\Ph return $this->functionReflectionFactory->create( $reflectionFunction, $templateTypeMap, - array_map(static function (ParamTag $paramTag): Type { - return $paramTag->getType(); - }, $phpDocParameterTags), + array_map(static fn (ParamTag $paramTag): Type => $paramTag->getType(), $phpDocParameterTags), $phpDocReturnTag !== null ? $phpDocReturnTag->getType() : null, $phpDocThrowsTag !== null ? $phpDocThrowsTag->getType() : null, $deprecatedTag !== null ? $deprecatedTag->getMessage() : null, $isDeprecated, $isInternal, $isFinal, - $reflectionFunction->getFileName(), - $isPure + $reflectionFunction->getFileName() !== false ? $reflectionFunction->getFileName() : null, + $isPure, ); } - public function resolveFunctionName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string + public function resolveFunctionName(Node\Name $nameNode, ?Scope $scope): ?string { return $this->resolveName($nameNode, function (string $name): bool { try { - $this->functionReflector->reflect($name); + $this->reflector->reflectFunction($name); return true; - } catch (\PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound $e) { + } catch (IdentifierNotFound) { // pass - } catch (InvalidIdentifierName $e) { + } catch (InvalidIdentifierName) { // pass } @@ -338,44 +310,48 @@ public function resolveFunctionName(\PhpParser\Node\Name $nameNode, ?Scope $scop }, $scope); } - public function hasConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool + public function hasConstant(Node\Name $nameNode, ?Scope $scope): bool { return $this->resolveConstantName($nameNode, $scope) !== null; } - public function getConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection + public function getConstant(Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection { $constantName = $this->resolveConstantName($nameNode, $scope); if ($constantName === null) { - throw new \PHPStan\Broker\ConstantNotFoundException((string) $nameNode); + throw new ConstantNotFoundException((string) $nameNode); + } + + if (array_key_exists($constantName, $this->cachedConstants)) { + return $this->cachedConstants[$constantName]; } - $constantReflection = $this->constantReflector->reflect($constantName); + $constantReflection = $this->reflector->reflectConstant($constantName); try { $constantValue = $constantReflection->getValue(); $constantValueType = ConstantTypeHelper::getTypeFromValue($constantValue); $fileName = $constantReflection->getFileName(); - } catch (UnableToCompileNode | NotAClassReflection | NotAnInterfaceReflection $e) { + } catch (UnableToCompileNode | NotAClassReflection | NotAnInterfaceReflection) { $constantValueType = new MixedType(); $fileName = null; } - return new RuntimeConstantReflection( + return $this->cachedConstants[$constantName] = new RuntimeConstantReflection( $constantName, $constantValueType, - $fileName + $fileName, ); } - public function resolveConstantName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string + public function resolveConstantName(Node\Name $nameNode, ?Scope $scope): ?string { return $this->resolveName($nameNode, function (string $name): bool { try { - $this->constantReflector->reflect($name); + $this->reflector->reflectConstant($name); return true; - } catch (\PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound $e) { + } catch (IdentifierNotFound) { // pass - } catch (UnableToCompileNode | NotAClassReflection | NotAnInterfaceReflection $e) { + } catch (UnableToCompileNode | NotAClassReflection | NotAnInterfaceReflection) { // pass } return false; @@ -383,15 +359,12 @@ public function resolveConstantName(\PhpParser\Node\Name $nameNode, ?Scope $scop } /** - * @param \PhpParser\Node\Name $nameNode - * @param \Closure(string $name): bool $existsCallback - * @param Scope|null $scope - * @return string|null + * @param Closure(string $name): bool $existsCallback */ private function resolveName( - \PhpParser\Node\Name $nameNode, - \Closure $existsCallback, - ?Scope $scope + Node\Name $nameNode, + Closure $existsCallback, + ?Scope $scope, ): ?string { $name = (string) $nameNode; diff --git a/src/Reflection/BetterReflection/BetterReflectionProviderFactory.php b/src/Reflection/BetterReflection/BetterReflectionProviderFactory.php index 903fc7c02b..b6fa9c0309 100644 --- a/src/Reflection/BetterReflection/BetterReflectionProviderFactory.php +++ b/src/Reflection/BetterReflection/BetterReflectionProviderFactory.php @@ -2,17 +2,13 @@ namespace PHPStan\Reflection\BetterReflection; -use PHPStan\BetterReflection\Reflector\ClassReflector; -use PHPStan\BetterReflection\Reflector\ConstantReflector; -use PHPStan\BetterReflection\Reflector\FunctionReflector; +use PHPStan\BetterReflection\Reflector\Reflector; interface BetterReflectionProviderFactory { public function create( - FunctionReflector $functionReflector, - ClassReflector $classReflector, - ConstantReflector $constantReflector + Reflector $reflector, ): BetterReflectionProvider; } diff --git a/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php b/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php index ad3962e587..8e88819ffa 100644 --- a/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php +++ b/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php @@ -2,6 +2,8 @@ namespace PHPStan\Reflection\BetterReflection; +use PhpParser\Node; + use PHPStan\DependencyInjection\Container; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\BetterReflection\SourceLocator\AutoloadSourceLocator; @@ -12,7 +14,6 @@ use PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocatorRepository; use PHPStan\Reflection\BetterReflection\SourceLocator\PhpVersionBlacklistSourceLocator; use PHPStan\Reflection\BetterReflection\SourceLocator\SkipClassAliasSourceLocator; -use PHPStan\BetterReflection\Reflector\FunctionReflector; use PHPStan\BetterReflection\SourceLocator\Ast\Locator; use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; use PHPStan\BetterReflection\SourceLocator\SourceStubber\ReflectionSourceStubber; @@ -50,15 +51,6 @@ class BetterReflectionSourceLocatorFactory /** @var AutoloadSourceLocator */ private $autoloadSourceLocator; - /** @var \PHPStan\DependencyInjection\Container */ - private $container; - - /** @var string[] */ - private $autoloadDirectories; - - /** @var string[] */ - private $autoloadFiles; - /** @var string[] */ private $scanFiles; @@ -81,8 +73,6 @@ class BetterReflectionSourceLocatorFactory private array $staticReflectionClassNamePatterns; /** - * @param string[] $autoloadDirectories - * @param string[] $autoloadFiles * @param string[] $scanFiles * @param string[] $scanDirectories * @param string[] $analysedPaths @@ -100,9 +90,6 @@ public function __construct( OptimizedDirectorySourceLocatorRepository $optimizedDirectorySourceLocatorRepository, ComposerJsonAndInstalledJsonSourceLocatorMaker $composerJsonAndInstalledJsonSourceLocatorMaker, AutoloadSourceLocator $autoloadSourceLocator, - Container $container, - array $autoloadDirectories, - array $autoloadFiles, array $scanFiles, array $scanDirectories, array $analysedPaths, @@ -120,9 +107,6 @@ public function __construct( $this->optimizedDirectorySourceLocatorRepository = $optimizedDirectorySourceLocatorRepository; $this->composerJsonAndInstalledJsonSourceLocatorMaker = $composerJsonAndInstalledJsonSourceLocatorMaker; $this->autoloadSourceLocator = $autoloadSourceLocator; - $this->container = $container; - $this->autoloadDirectories = $autoloadDirectories; - $this->autoloadFiles = $autoloadFiles; $this->scanFiles = $scanFiles; $this->scanDirectories = $scanDirectories; $this->analysedPaths = $analysedPaths; @@ -156,23 +140,18 @@ public function create(): SourceLocator $analysedDirectories[] = $analysedPath; } - $analysedFiles = array_unique(array_merge($analysedFiles, $this->autoloadFiles, $this->scanFiles)); + $analysedFiles = array_unique(array_merge($analysedFiles, $this->scanFiles)); foreach ($analysedFiles as $analysedFile) { $locators[] = $this->optimizedSingleFileSourceLocatorRepository->getOrCreate($analysedFile); } - $directories = array_unique(array_merge($analysedDirectories, $this->autoloadDirectories, $this->scanDirectories)); + $directories = array_unique(array_merge($analysedDirectories, $this->scanDirectories)); foreach ($directories as $directory) { $locators[] = $this->optimizedDirectorySourceLocatorRepository->getOrCreate($directory); } - $astLocator = new Locator($this->parser, function (): FunctionReflector { - return $this->container->getService('betterReflectionFunctionReflector'); - }); - - $astPhp8Locator = new Locator($this->php8Parser, function (): FunctionReflector { - return $this->container->getService('betterReflectionFunctionReflector'); - }); + $astLocator = new Locator($this->parser); + $astPhp8Locator = new Locator($this->php8Parser); $locators[] = new SkipClassAliasSourceLocator(new PhpInternalSourceLocator($astPhp8Locator, $this->phpstormStubsSourceStubber)); $locators[] = new ClassBlacklistSourceLocator($this->autoloadSourceLocator, $this->staticReflectionClassNamePatterns); diff --git a/src/Reflection/BetterReflection/Reflector/MemoizingClassReflector.php b/src/Reflection/BetterReflection/Reflector/MemoizingClassReflector.php deleted file mode 100644 index d34a77a86f..0000000000 --- a/src/Reflection/BetterReflection/Reflector/MemoizingClassReflector.php +++ /dev/null @@ -1,39 +0,0 @@ - */ - private array $reflections = []; - - /** - * Create a ReflectionClass for the specified $className. - * - * @return \PHPStan\BetterReflection\Reflection\ReflectionClass - * - * @throws \PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound - */ - public function reflect(string $className): Reflection - { - $lowerClassName = strtolower($className); - if (isset($this->reflections[$lowerClassName])) { - if ($this->reflections[$lowerClassName] instanceof \Throwable) { - throw $this->reflections[$lowerClassName]; - } - return $this->reflections[$lowerClassName]; - } - - try { - return $this->reflections[$lowerClassName] = parent::reflect($className); - } catch (\Throwable $e) { - $this->reflections[$lowerClassName] = $e; - throw $e; - } - } - -} diff --git a/src/Reflection/BetterReflection/Reflector/MemoizingConstantReflector.php b/src/Reflection/BetterReflection/Reflector/MemoizingConstantReflector.php deleted file mode 100644 index aca5db5d44..0000000000 --- a/src/Reflection/BetterReflection/Reflector/MemoizingConstantReflector.php +++ /dev/null @@ -1,38 +0,0 @@ - */ - private array $reflections = []; - - /** - * Create a ReflectionConstant for the specified $constantName. - * - * @return \PHPStan\BetterReflection\Reflection\ReflectionConstant - * - * @throws \PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound - */ - public function reflect(string $constantName): Reflection - { - if (isset($this->reflections[$constantName])) { - if ($this->reflections[$constantName] instanceof \Throwable) { - throw $this->reflections[$constantName]; - } - return $this->reflections[$constantName]; - } - - try { - return $this->reflections[$constantName] = parent::reflect($constantName); - } catch (\Throwable $e) { - $this->reflections[$constantName] = $e; - throw $e; - } - } - -} diff --git a/src/Reflection/BetterReflection/Reflector/MemoizingFunctionReflector.php b/src/Reflection/BetterReflection/Reflector/MemoizingFunctionReflector.php deleted file mode 100644 index 078392ef1b..0000000000 --- a/src/Reflection/BetterReflection/Reflector/MemoizingFunctionReflector.php +++ /dev/null @@ -1,39 +0,0 @@ - */ - private array $reflections = []; - - /** - * Create a ReflectionFunction for the specified $functionName. - * - * @return \PHPStan\BetterReflection\Reflection\ReflectionFunction - * - * @throws \PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound - */ - public function reflect(string $functionName): Reflection - { - $lowerFunctionName = strtolower($functionName); - if (isset($this->reflections[$lowerFunctionName])) { - if ($this->reflections[$lowerFunctionName] instanceof \Throwable) { - throw $this->reflections[$lowerFunctionName]; - } - return $this->reflections[$lowerFunctionName]; - } - - try { - return $this->reflections[$lowerFunctionName] = parent::reflect($functionName); - } catch (\Throwable $e) { - $this->reflections[$lowerFunctionName] = $e; - throw $e; - } - } - -} diff --git a/src/Reflection/BetterReflection/Reflector/MemoizingReflector.php b/src/Reflection/BetterReflection/Reflector/MemoizingReflector.php new file mode 100644 index 0000000000..486ad45c20 --- /dev/null +++ b/src/Reflection/BetterReflection/Reflector/MemoizingReflector.php @@ -0,0 +1,111 @@ + */ + private array $classReflections = []; + + /** @var array */ + private array $constantReflections = []; + + /** @var array */ + private array $functionReflections = []; + + public function __construct(private Reflector $reflector) + { + } + + public function reflectClass(string $className): ReflectionClass + { + $lowerClassName = strtolower($className); + if (array_key_exists($lowerClassName, $this->classReflections) && $this->classReflections[$lowerClassName] !== null) { + return $this->classReflections[$lowerClassName]; + } + if (array_key_exists($className, $this->classReflections)) { + $classReflection = $this->classReflections[$className]; + if ($classReflection === null) { + throw IdentifierNotFound::fromIdentifier(new Identifier($className, new IdentifierType(IdentifierType::IDENTIFIER_CLASS))); + } + + return $classReflection; + } + + try { + return $this->classReflections[$lowerClassName] = $this->reflector->reflectClass($className); + } catch (IdentifierNotFound $e) { + $this->classReflections[$className] = null; + + throw $e; + } + } + + public function reflectConstant(string $constantName): ReflectionConstant + { + if (array_key_exists($constantName, $this->constantReflections)) { + $constantReflection = $this->constantReflections[$constantName]; + if ($constantReflection === null) { + throw IdentifierNotFound::fromIdentifier(new Identifier($constantName, new IdentifierType(IdentifierType::IDENTIFIER_CONSTANT))); + } + + return $constantReflection; + } + + try { + return $this->constantReflections[$constantName] = $this->reflector->reflectConstant($constantName); + } catch (IdentifierNotFound $e) { + $this->constantReflections[$constantName] = null; + + throw $e; + } + } + + public function reflectFunction(string $functionName): ReflectionFunction + { + $lowerFunctionName = strtolower($functionName); + if (array_key_exists($lowerFunctionName, $this->functionReflections)) { + $functionReflection = $this->functionReflections[$lowerFunctionName]; + if ($functionReflection === null) { + throw IdentifierNotFound::fromIdentifier(new Identifier($functionName, new IdentifierType(IdentifierType::IDENTIFIER_FUNCTION))); + } + + return $functionReflection; + } + + try { + return $this->functionReflections[$lowerFunctionName] = $this->reflector->reflectFunction($functionName); + } catch (IdentifierNotFound $e) { + $this->functionReflections[$lowerFunctionName] = null; + + throw $e; + } + } + + public function reflectAllClasses(): iterable + { + return $this->reflector->reflectAllClasses(); + } + + public function reflectAllFunctions(): iterable + { + return $this->reflector->reflectAllFunctions(); + } + + public function reflectAllConstants(): iterable + { + return $this->reflector->reflectAllConstants(); + } + +} diff --git a/src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php index f88f060808..aedb8f39f0 100644 --- a/src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; +use PhpParser\Node; use PhpParser\Node\Arg; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Name; @@ -14,11 +15,25 @@ use PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection; use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; +use PHPStan\ShouldNotHappenException; use ReflectionClass; use ReflectionFunction; use function array_key_exists; -use function file_exists; +use function array_keys; +use function class_exists; +use function constant; +use function count; +use function defined; +use function function_exists; +use function interface_exists; +use function is_file; +use function is_string; use function restore_error_handler; +use function set_error_handler; +use function spl_autoload_functions; +use function strtolower; +use function trait_exists; +use const PHP_VERSION_ID; /** * Use PHP's built in autoloader to locate a class, without actually loading. @@ -31,26 +46,23 @@ class AutoloadSourceLocator implements SourceLocator { - private FileNodesFetcher $fileNodesFetcher; - - /** @var array>> */ + /** @var array>> */ private array $classNodes = []; /** @var array */ private array $classReflections = []; - /** @var array> */ + /** @var array> */ private array $functionNodes = []; - /** @var array> */ + /** @var array> */ private array $constantNodes = []; - /** @var array */ - private array $locatedSourcesByFile = []; + /** @var array */ + private array $fetchedNodesByFile = []; - public function __construct(FileNodesFetcher $fileNodesFetcher) + public function __construct(private FileNodesFetcher $fileNodesFetcher, private bool $disableRuntimeReflectionProvider) { - $this->fileNodesFetcher = $fileNodesFetcher; } public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection @@ -63,8 +75,8 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): return $nodeToReflection->__invoke( $reflector, $this->functionNodes[$loweredFunctionName]->getNode(), - $this->locatedSourcesByFile[$this->functionNodes[$loweredFunctionName]->getFileName()], - $this->functionNodes[$loweredFunctionName]->getNamespace() + $this->functionNodes[$loweredFunctionName]->getLocatedSource(), + $this->functionNodes[$loweredFunctionName]->getNamespace(), ); } if (!function_exists($functionName)) { @@ -77,7 +89,7 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): if (!is_string($reflectionFileName)) { return null; } - if (!file_exists($reflectionFileName)) { + if (!is_file($reflectionFileName)) { return null; } @@ -92,14 +104,11 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): $constantReflection = $nodeToReflection->__invoke( $reflector, $stmtConst->getNode(), - $this->locatedSourcesByFile[$stmtConst->getFileName()], - $stmtConst->getNamespace() + $stmtConst->getLocatedSource(), + $stmtConst->getNamespace(), ); - if ($constantReflection === null) { - continue; - } if (!$constantReflection instanceof ReflectionConstant) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ($constantReflection->getName() !== $identifier->getName()) { continue; @@ -112,15 +121,12 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): $constantReflection = $nodeToReflection->__invoke( $reflector, $stmtConst->getNode(), - $this->locatedSourcesByFile[$stmtConst->getFileName()], + $stmtConst->getLocatedSource(), $stmtConst->getNamespace(), - $i + $i, ); - if ($constantReflection === null) { - continue; - } if (!$constantReflection instanceof ReflectionConstant) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ($constantReflection->getName() !== $identifier->getName()) { continue; @@ -140,11 +146,11 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): new Arg(new String_($constantName)), new Arg(new String_('')), // not actually used ]), - new LocatedSource('', null), + new LocatedSource('', $constantName, null), + null, null, - null ); - $reflection->populateValue(constant($constantName)); + $reflection->populateValue(@constant($constantName)); return $reflection; } @@ -166,8 +172,8 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): return $this->classReflections[$loweredClassName] = $nodeToReflection->__invoke( $reflector, $classNode->getNode(), - $this->locatedSourcesByFile[$classNode->getFileName()], - $classNode->getNamespace() + $classNode->getLocatedSource(), + $classNode->getNamespace(), ); } } @@ -180,9 +186,8 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): private function findReflection(Reflector $reflector, string $file, Identifier $identifier, ?int $startLine): ?Reflection { - if (!array_key_exists($file, $this->locatedSourcesByFile)) { + if (!array_key_exists($file, $this->fetchedNodesByFile)) { $result = $this->fileNodesFetcher->fetchNodes($file); - $this->locatedSourcesByFile[$file] = $result->getLocatedSource(); foreach ($result->getClassNodes() as $className => $fetchedClassNodes) { foreach ($fetchedClassNodes as $fetchedClassNode) { $this->classNodes[$className][] = $fetchedClassNode; @@ -194,9 +199,8 @@ private function findReflection(Reflector $reflector, string $file, Identifier $ foreach ($result->getConstantNodes() as $fetchedConstantNode) { $this->constantNodes[] = $fetchedConstantNode; } - $locatedSource = $result->getLocatedSource(); - } else { - $locatedSource = $this->locatedSourcesByFile[$file]; + + $this->fetchedNodesByFile[$file] = true; } $nodeToReflection = new NodeToReflection(); @@ -209,8 +213,9 @@ private function findReflection(Reflector $reflector, string $file, Identifier $ return null; } + $classNodesCount = count($this->classNodes[$identifierName]); foreach ($this->classNodes[$identifierName] as $classNode) { - if ($startLine !== null) { + if ($classNodesCount > 1 && $startLine !== null) { if (count($classNode->getNode()->attrGroups) > 0 && PHP_VERSION_ID < 80000) { $startLine--; } @@ -222,8 +227,8 @@ private function findReflection(Reflector $reflector, string $file, Identifier $ return $this->classReflections[$identifierName] = $nodeToReflection->__invoke( $reflector, $classNode->getNode(), - $locatedSource, - $classNode->getNamespace() + $classNode->getLocatedSource(), + $classNode->getNamespace(), ); } @@ -238,8 +243,8 @@ private function findReflection(Reflector $reflector, string $file, Identifier $ return $nodeToReflection->__invoke( $reflector, $this->functionNodes[$identifierName]->getNode(), - $locatedSource, - $this->functionNodes[$identifierName]->getNamespace() + $this->functionNodes[$identifierName]->getLocatedSource(), + $this->functionNodes[$identifierName]->getNamespace(), ); } @@ -268,7 +273,7 @@ public function locateIdentifiersByType(Reflector $reflector, IdentifierType $id */ private function locateClassByName(string $className): ?array { - if (class_exists($className, false) || interface_exists($className, false) || trait_exists($className, false)) { + if (class_exists($className, !$this->disableRuntimeReflectionProvider) || interface_exists($className, !$this->disableRuntimeReflectionProvider) || trait_exists($className, !$this->disableRuntimeReflectionProvider)) { $reflection = new ReflectionClass($className); $filename = $reflection->getFileName(); @@ -276,13 +281,17 @@ private function locateClassByName(string $className): ?array return null; } - if (!file_exists($filename)) { + if (!is_file($filename)) { return null; } return [$filename, $reflection->getName(), $reflection->getStartLine() !== false ? $reflection->getStartLine() : null]; } + if (!$this->disableRuntimeReflectionProvider) { + return null; + } + $this->silenceErrors(); try { @@ -309,7 +318,7 @@ static function () use ($className): ?array { } return null; - } + }, ); } finally { restore_error_handler(); @@ -318,9 +327,7 @@ static function () use ($className): ?array { private function silenceErrors(): void { - set_error_handler(static function (): bool { - return true; - }); + set_error_handler(static fn (): bool => true); } } diff --git a/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php b/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php index 30ef0852e4..c0ba0501f0 100644 --- a/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php +++ b/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php @@ -3,34 +3,42 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; use PhpParser\BuilderHelpers; +use PhpParser\Node; use PhpParser\Node\Stmt\Namespace_; +use PhpParser\NodeTraverser; use PhpParser\NodeVisitorAbstract; use PHPStan\BetterReflection\Reflection\Exception\InvalidConstantNode; +use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource; use PHPStan\BetterReflection\Util\ConstantNodeChecker; +use function constant; +use function defined; +use function strtolower; class CachingVisitor extends NodeVisitorAbstract { private string $fileName; - /** @var array>> */ + private string $contents; + + /** @var array>> */ private array $classNodes; - /** @var array> */ + /** @var array> */ private array $functionNodes; - /** @var array> */ + /** @var array> */ private array $constantNodes; - private ?\PhpParser\Node\Stmt\Namespace_ $currentNamespaceNode = null; + private ?Node\Stmt\Namespace_ $currentNamespaceNode = null; - public function enterNode(\PhpParser\Node $node): ?int + public function enterNode(Node $node): ?int { if ($node instanceof Namespace_) { $this->currentNamespaceNode = $node; } - if ($node instanceof \PhpParser\Node\Stmt\ClassLike) { + if ($node instanceof Node\Stmt\ClassLike) { if ($node->name !== null) { $fullClassName = $node->name->toString(); if ($this->currentNamespaceNode !== null && $this->currentNamespaceNode->name !== null) { @@ -39,67 +47,73 @@ public function enterNode(\PhpParser\Node $node): ?int $this->classNodes[strtolower($fullClassName)][] = new FetchedNode( $node, $this->currentNamespaceNode, - $this->fileName + $this->fileName, + new LocatedSource($this->contents, $fullClassName, $this->fileName), ); } - return \PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN; + return NodeTraverser::DONT_TRAVERSE_CHILDREN; } - if ($node instanceof \PhpParser\Node\Stmt\Function_) { - $this->functionNodes[strtolower($node->namespacedName->toString())] = new FetchedNode( - $node, - $this->currentNamespaceNode, - $this->fileName - ); + if ($node instanceof Node\Stmt\Function_) { + if ($node->namespacedName !== null) { + $functionName = $node->namespacedName->toString(); + $this->functionNodes[strtolower($functionName)] = new FetchedNode( + $node, + $this->currentNamespaceNode, + $this->fileName, + new LocatedSource($this->contents, $functionName, $this->fileName), + ); + } - return \PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN; + return NodeTraverser::DONT_TRAVERSE_CHILDREN; } - if ($node instanceof \PhpParser\Node\Stmt\Const_) { + if ($node instanceof Node\Stmt\Const_) { $this->constantNodes[] = new FetchedNode( $node, $this->currentNamespaceNode, - $this->fileName + $this->fileName, + new LocatedSource($this->contents, null, $this->fileName), ); - return \PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN; + return NodeTraverser::DONT_TRAVERSE_CHILDREN; } - if ($node instanceof \PhpParser\Node\Expr\FuncCall) { + if ($node instanceof Node\Expr\FuncCall) { try { ConstantNodeChecker::assertValidDefineFunctionCall($node); - } catch (InvalidConstantNode $e) { + } catch (InvalidConstantNode) { return null; } - /** @var \PhpParser\Node\Scalar\String_ $nameNode */ - $nameNode = $node->args[0]->value; + /** @var Node\Scalar\String_ $nameNode */ + $nameNode = $node->getArgs()[0]->value; $constantName = $nameNode->value; if (defined($constantName)) { - $constantValue = constant($constantName); - $node->args[1]->value = BuilderHelpers::normalizeValue($constantValue); + $constantValue = @constant($constantName); + $node->getArgs()[1]->value = BuilderHelpers::normalizeValue($constantValue); } $constantNode = new FetchedNode( $node, $this->currentNamespaceNode, - $this->fileName + $this->fileName, + new LocatedSource($this->contents, $constantName, $this->fileName), ); $this->constantNodes[] = $constantNode; - return \PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN; + return NodeTraverser::DONT_TRAVERSE_CHILDREN; } return null; } /** - * @param \PhpParser\Node $node * @return null */ - public function leaveNode(\PhpParser\Node $node) + public function leaveNode(Node $node) { if (!$node instanceof Namespace_) { return null; @@ -110,7 +124,7 @@ public function leaveNode(\PhpParser\Node $node) } /** - * @return array>> + * @return array>> */ public function getClassNodes(): array { @@ -118,7 +132,7 @@ public function getClassNodes(): array } /** - * @return array> + * @return array> */ public function getFunctionNodes(): array { @@ -126,19 +140,20 @@ public function getFunctionNodes(): array } /** - * @return array> + * @return array> */ public function getConstantNodes(): array { return $this->constantNodes; } - public function reset(string $fileName): void + public function reset(string $fileName, string $contents): void { $this->classNodes = []; $this->functionNodes = []; $this->constantNodes = []; $this->fileName = $fileName; + $this->contents = $contents; } } diff --git a/src/Reflection/BetterReflection/SourceLocator/ClassBlacklistSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/ClassBlacklistSourceLocator.php index 9b4ca0d402..08972e2a6b 100644 --- a/src/Reflection/BetterReflection/SourceLocator/ClassBlacklistSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/ClassBlacklistSourceLocator.php @@ -12,22 +12,14 @@ class ClassBlacklistSourceLocator implements SourceLocator { - private SourceLocator $sourceLocator; - - /** @var string[] */ - private array $patterns; - /** - * @param SourceLocator $sourceLocator * @param string[] $patterns */ public function __construct( - SourceLocator $sourceLocator, - array $patterns + private SourceLocator $sourceLocator, + private array $patterns, ) { - $this->sourceLocator = $sourceLocator; - $this->patterns = $patterns; } public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection diff --git a/src/Reflection/BetterReflection/SourceLocator/ClassWhitelistSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/ClassWhitelistSourceLocator.php index cff56df029..3dd35fe193 100644 --- a/src/Reflection/BetterReflection/SourceLocator/ClassWhitelistSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/ClassWhitelistSourceLocator.php @@ -12,22 +12,14 @@ class ClassWhitelistSourceLocator implements SourceLocator { - private SourceLocator $sourceLocator; - - /** @var string[] */ - private array $patterns; - /** - * @param SourceLocator $sourceLocator * @param string[] $patterns */ public function __construct( - SourceLocator $sourceLocator, - array $patterns + private SourceLocator $sourceLocator, + private array $patterns, ) { - $this->sourceLocator = $sourceLocator; - $this->patterns = $patterns; } public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection diff --git a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php index cc0e55dea6..93172ebe37 100644 --- a/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php +++ b/src/Reflection/BetterReflection/SourceLocator/ComposerJsonAndInstalledJsonSourceLocatorMaker.php @@ -3,30 +3,32 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; use Nette\Utils\Json; +use Nette\Utils\JsonException; use PHPStan\BetterReflection\SourceLocator\Type\AggregateSourceLocator; use PHPStan\BetterReflection\SourceLocator\Type\Composer\Psr\Psr0Mapping; use PHPStan\BetterReflection\SourceLocator\Type\Composer\Psr\Psr4Mapping; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; +use PHPStan\File\CouldNotReadFileException; use PHPStan\File\FileReader; +use function array_filter; +use function array_key_exists; +use function array_map; +use function array_merge; +use function array_merge_recursive; +use function count; +use function dirname; +use function is_dir; +use function is_file; class ComposerJsonAndInstalledJsonSourceLocatorMaker { - private \PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedDirectorySourceLocatorRepository $optimizedDirectorySourceLocatorRepository; - - private \PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedPsrAutoloaderLocatorFactory $optimizedPsrAutoloaderLocatorFactory; - - private OptimizedDirectorySourceLocatorFactory $optimizedDirectorySourceLocatorFactory; - public function __construct( - OptimizedDirectorySourceLocatorRepository $optimizedDirectorySourceLocatorRepository, - OptimizedPsrAutoloaderLocatorFactory $optimizedPsrAutoloaderLocatorFactory, - OptimizedDirectorySourceLocatorFactory $optimizedDirectorySourceLocatorFactory + private OptimizedDirectorySourceLocatorRepository $optimizedDirectorySourceLocatorRepository, + private OptimizedPsrAutoloaderLocatorFactory $optimizedPsrAutoloaderLocatorFactory, + private OptimizedDirectorySourceLocatorFactory $optimizedDirectorySourceLocatorFactory, ) { - $this->optimizedDirectorySourceLocatorRepository = $optimizedDirectorySourceLocatorRepository; - $this->optimizedPsrAutoloaderLocatorFactory = $optimizedPsrAutoloaderLocatorFactory; - $this->optimizedDirectorySourceLocatorFactory = $optimizedDirectorySourceLocatorFactory; } public function create(string $projectInstallationPath): ?SourceLocator @@ -45,67 +47,64 @@ public function create(string $projectInstallationPath): ?SourceLocator try { $composerJsonContents = FileReader::read($composerJsonPath); $composer = Json::decode($composerJsonContents, Json::FORCE_ARRAY); - } catch (\PHPStan\File\CouldNotReadFileException | \Nette\Utils\JsonException $e) { + } catch (CouldNotReadFileException | JsonException) { return null; } try { $installedJsonContents = FileReader::read($installedJsonPath); $installedJson = Json::decode($installedJsonContents, Json::FORCE_ARRAY); - } catch (\PHPStan\File\CouldNotReadFileException | \Nette\Utils\JsonException $e) { + } catch (CouldNotReadFileException | JsonException) { return null; } $installed = $installedJson['packages'] ?? $installedJson; + $dev = (bool) ($installedJson['dev'] ?? true); $classMapPaths = array_merge( $this->prefixPaths($this->packageToClassMapPaths($composer), $projectInstallationPath . '/'), - ...array_map(function (array $package) use ($projectInstallationPath, $installedJsonDirectoryPath): array { - return $this->prefixPaths( - $this->packageToClassMapPaths($package), - $this->packagePrefixPath($projectInstallationPath, $installedJsonDirectoryPath, $package) - ); - }, $installed) + $dev ? $this->prefixPaths($this->packageToClassMapPaths($composer, 'autoload-dev'), $projectInstallationPath . '/') : [], + ...array_map(fn (array $package): array => $this->prefixPaths( + $this->packageToClassMapPaths($package), + $this->packagePrefixPath($projectInstallationPath, $installedJsonDirectoryPath, $package), + ), $installed), ); $classMapFiles = array_filter($classMapPaths, 'is_file'); $classMapDirectories = array_filter($classMapPaths, 'is_dir'); $filePaths = array_merge( $this->prefixPaths($this->packageToFilePaths($composer), $projectInstallationPath . '/'), - ...array_map(function (array $package) use ($projectInstallationPath, $installedJsonDirectoryPath): array { - return $this->prefixPaths( - $this->packageToFilePaths($package), - $this->packagePrefixPath($projectInstallationPath, $installedJsonDirectoryPath, $package) - ); - }, $installed) + $dev ? $this->prefixPaths($this->packageToFilePaths($composer, 'autoload-dev'), $projectInstallationPath . '/') : [], + ...array_map(fn (array $package): array => $this->prefixPaths( + $this->packageToFilePaths($package), + $this->packagePrefixPath($projectInstallationPath, $installedJsonDirectoryPath, $package), + ), $installed), ); $locators = []; $locators[] = $this->optimizedPsrAutoloaderLocatorFactory->create( Psr4Mapping::fromArrayMappings(array_merge_recursive( $this->prefixWithInstallationPath($this->packageToPsr4AutoloadNamespaces($composer), $projectInstallationPath), - ...array_map(function (array $package) use ($projectInstallationPath, $installedJsonDirectoryPath): array { - return $this->prefixWithPackagePath( - $this->packageToPsr4AutoloadNamespaces($package), - $projectInstallationPath, - $installedJsonDirectoryPath, - $package - ); - }, $installed) - )) + $dev ? $this->prefixWithInstallationPath($this->packageToPsr4AutoloadNamespaces($composer, 'autoload-dev'), $projectInstallationPath) : [], + ...array_map(fn (array $package): array => $this->prefixWithPackagePath( + $this->packageToPsr4AutoloadNamespaces($package), + $projectInstallationPath, + $installedJsonDirectoryPath, + $package, + ), $installed), + )), ); $locators[] = $this->optimizedPsrAutoloaderLocatorFactory->create( Psr0Mapping::fromArrayMappings(array_merge_recursive( $this->prefixWithInstallationPath($this->packageToPsr0AutoloadNamespaces($composer), $projectInstallationPath), - ...array_map(function (array $package) use ($projectInstallationPath, $installedJsonDirectoryPath): array { - return $this->prefixWithPackagePath( - $this->packageToPsr0AutoloadNamespaces($package), - $projectInstallationPath, - $installedJsonDirectoryPath, - $package - ); - }, $installed) - )) + $dev ? $this->prefixWithInstallationPath($this->packageToPsr0AutoloadNamespaces($composer, 'autoload-dev'), $projectInstallationPath) : [], + ...array_map(fn (array $package): array => $this->prefixWithPackagePath( + $this->packageToPsr0AutoloadNamespaces($package), + $projectInstallationPath, + $installedJsonDirectoryPath, + $package, + ), $installed), + )), ); foreach ($classMapDirectories as $classMapDirectory) { @@ -136,11 +135,9 @@ public function create(string $projectInstallationPath): ?SourceLocator * * @return array> */ - private function packageToPsr4AutoloadNamespaces(array $package): array + private function packageToPsr4AutoloadNamespaces(array $package, string $autoloadSection = 'autoload'): array { - return array_map(static function ($namespacePaths): array { - return (array) $namespacePaths; - }, $package['autoload']['psr-4'] ?? []); + return array_map(static fn ($namespacePaths): array => (array) $namespacePaths, $package[$autoloadSection]['psr-4'] ?? []); } /** @@ -148,11 +145,9 @@ private function packageToPsr4AutoloadNamespaces(array $package): array * * @return array> */ - private function packageToPsr0AutoloadNamespaces(array $package): array + private function packageToPsr0AutoloadNamespaces(array $package, string $autoloadSection = 'autoload'): array { - return array_map(static function ($namespacePaths): array { - return (array) $namespacePaths; - }, $package['autoload']['psr-0'] ?? []); + return array_map(static fn ($namespacePaths): array => (array) $namespacePaths, $package[$autoloadSection]['psr-0'] ?? []); } /** @@ -160,9 +155,9 @@ private function packageToPsr0AutoloadNamespaces(array $package): array * * @return array */ - private function packageToClassMapPaths(array $package): array + private function packageToClassMapPaths(array $package, string $autoloadSection = 'autoload'): array { - return $package['autoload']['classmap'] ?? []; + return $package[$autoloadSection]['classmap'] ?? []; } /** @@ -170,9 +165,9 @@ private function packageToClassMapPaths(array $package): array * * @return array */ - private function packageToFilePaths(array $package): array + private function packageToFilePaths(array $package, string $autoloadSection = 'autoload'): array { - return $package['autoload']['files'] ?? []; + return $package[$autoloadSection]['files'] ?? []; } /** @@ -181,7 +176,7 @@ private function packageToFilePaths(array $package): array private function packagePrefixPath( string $projectInstallationPath, string $installedJsonDirectoryPath, - array $package + array $package, ): string { if (array_key_exists('install-path', $package)) { @@ -201,9 +196,7 @@ private function prefixWithPackagePath(array $paths, string $projectInstallation { $prefix = $this->packagePrefixPath($projectInstallationPath, $installedJsonDirectoryPath, $package); - return array_map(function (array $paths) use ($prefix): array { - return $this->prefixPaths($paths, $prefix); - }, $paths); + return array_map(fn (array $paths): array => $this->prefixPaths($paths, $prefix), $paths); } /** @@ -213,9 +206,7 @@ private function prefixWithPackagePath(array $paths, string $projectInstallation */ private function prefixWithInstallationPath(array $paths, string $trimmedInstallationPath): array { - return array_map(function (array $paths) use ($trimmedInstallationPath): array { - return $this->prefixPaths($paths, $trimmedInstallationPath . '/'); - }, $paths); + return array_map(fn (array $paths): array => $this->prefixPaths($paths, $trimmedInstallationPath . '/'), $paths); } /** @@ -225,9 +216,7 @@ private function prefixWithInstallationPath(array $paths, string $trimmedInstall */ private function prefixPaths(array $paths, string $prefix): array { - return array_map(static function (string $path) use ($prefix): string { - return $prefix . $path; - }, $paths); + return array_map(static fn (string $path): string => $prefix . $path, $paths); } } diff --git a/src/Reflection/BetterReflection/SourceLocator/FetchedNode.php b/src/Reflection/BetterReflection/SourceLocator/FetchedNode.php index c5a48bf720..5af9211c14 100644 --- a/src/Reflection/BetterReflection/SourceLocator/FetchedNode.php +++ b/src/Reflection/BetterReflection/SourceLocator/FetchedNode.php @@ -2,44 +2,36 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; +use PhpParser\Node; +use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource; + /** - * @template-covariant T of \PhpParser\Node + * @template-covariant T of Node */ class FetchedNode { - /** @var T */ - private \PhpParser\Node $node; - - private ?\PhpParser\Node\Stmt\Namespace_ $namespace; - - private string $fileName; - /** * @param T $node - * @param \PhpParser\Node\Stmt\Namespace_|null $namespace - * @param string $fileName */ public function __construct( - \PhpParser\Node $node, - ?\PhpParser\Node\Stmt\Namespace_ $namespace, - string $fileName + private Node $node, + private ?Node\Stmt\Namespace_ $namespace, + private string $fileName, + private LocatedSource $locatedSource, ) { - $this->node = $node; - $this->namespace = $namespace; - $this->fileName = $fileName; } /** * @return T */ - public function getNode(): \PhpParser\Node + public function getNode(): Node { return $this->node; } - public function getNamespace(): ?\PhpParser\Node\Stmt\Namespace_ + public function getNamespace(): ?Node\Stmt\Namespace_ { return $this->namespace; } @@ -49,4 +41,9 @@ public function getFileName(): string return $this->fileName; } + public function getLocatedSource(): LocatedSource + { + return $this->locatedSource; + } + } diff --git a/src/Reflection/BetterReflection/SourceLocator/FetchedNodesResult.php b/src/Reflection/BetterReflection/SourceLocator/FetchedNodesResult.php index 5791a790b6..af65371f20 100644 --- a/src/Reflection/BetterReflection/SourceLocator/FetchedNodesResult.php +++ b/src/Reflection/BetterReflection/SourceLocator/FetchedNodesResult.php @@ -2,43 +2,26 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; -use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource; +use PhpParser\Node; class FetchedNodesResult { - /** @var array>> */ - private array $classNodes; - - /** @var array> */ - private array $functionNodes; - - /** @var array> */ - private array $constantNodes; - - private \PHPStan\BetterReflection\SourceLocator\Located\LocatedSource $locatedSource; - /** - * @param array>> $classNodes - * @param array> $functionNodes - * @param array> $constantNodes - * @param \PHPStan\BetterReflection\SourceLocator\Located\LocatedSource $locatedSource + * @param array>> $classNodes + * @param array> $functionNodes + * @param array> $constantNodes */ public function __construct( - array $classNodes, - array $functionNodes, - array $constantNodes, - LocatedSource $locatedSource + private array $classNodes, + private array $functionNodes, + private array $constantNodes, ) { - $this->classNodes = $classNodes; - $this->functionNodes = $functionNodes; - $this->constantNodes = $constantNodes; - $this->locatedSource = $locatedSource; } /** - * @return array>> + * @return array>> */ public function getClassNodes(): array { @@ -46,7 +29,7 @@ public function getClassNodes(): array } /** - * @return array> + * @return array> */ public function getFunctionNodes(): array { @@ -54,16 +37,11 @@ public function getFunctionNodes(): array } /** - * @return array> + * @return array> */ public function getConstantNodes(): array { return $this->constantNodes; } - public function getLocatedSource(): LocatedSource - { - return $this->locatedSource; - } - } diff --git a/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php b/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php index db88b2b177..bd7f0c77dd 100644 --- a/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php +++ b/src/Reflection/BetterReflection/SourceLocator/FileNodesFetcher.php @@ -2,25 +2,20 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; +use PhpParser\Node; use PhpParser\NodeTraverser; -use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource; use PHPStan\File\FileReader; use PHPStan\Parser\Parser; +use PHPStan\Parser\ParserErrorsException; class FileNodesFetcher { - private \PHPStan\Reflection\BetterReflection\SourceLocator\CachingVisitor $cachingVisitor; - - private Parser $parser; - public function __construct( - CachingVisitor $cachingVisitor, - Parser $parser + private CachingVisitor $cachingVisitor, + private Parser $parser, ) { - $this->cachingVisitor = $cachingVisitor; - $this->parser = $parser; } public function fetchNodes(string $fileName): FetchedNodesResult @@ -29,22 +24,20 @@ public function fetchNodes(string $fileName): FetchedNodesResult $nodeTraverser->addVisitor($this->cachingVisitor); $contents = FileReader::read($fileName); - $locatedSource = new LocatedSource($contents, $fileName); try { - /** @var \PhpParser\Node[] $ast */ + /** @var Node[] $ast */ $ast = $this->parser->parseFile($fileName); - } catch (\PHPStan\Parser\ParserErrorsException $e) { - return new FetchedNodesResult([], [], [], $locatedSource); + } catch (ParserErrorsException) { + return new FetchedNodesResult([], [], []); } - $this->cachingVisitor->reset($fileName); + $this->cachingVisitor->reset($fileName, $contents); $nodeTraverser->traverse($ast); return new FetchedNodesResult( $this->cachingVisitor->getClassNodes(), $this->cachingVisitor->getFunctionNodes(), $this->cachingVisitor->getConstantNodes(), - $locatedSource ); } diff --git a/src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php b/src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php index 7efba32de1..e4c01a64b3 100644 --- a/src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php +++ b/src/Reflection/BetterReflection/SourceLocator/FileReadTrapStreamWrapper.php @@ -2,10 +2,15 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; +use PHPStan\ShouldNotHappenException; use function stat; use function stream_wrapper_register; use function stream_wrapper_restore; use function stream_wrapper_unregister; +use const SEEK_CUR; +use const SEEK_END; +use const SEEK_SET; +use const STREAM_URL_STAT_QUIET; /** * This class will operate as a stream wrapper, intercepting any access to a file while @@ -46,7 +51,7 @@ final class FileReadTrapStreamWrapper */ public static function withStreamWrapperOverride( callable $executeMeWithinStreamWrapperOverride, - array $streamWrapperProtocols = self::DEFAULT_STREAM_WRAPPER_PROTOCOLS + array $streamWrapperProtocols = self::DEFAULT_STREAM_WRAPPER_PROTOCOLS, ) { self::$registeredStreamWrapperProtocols = $streamWrapperProtocols; @@ -101,7 +106,6 @@ public function stream_open($path, $mode, $options, &$openedPath): bool * * @param int $count * - * @return string */ public function stream_read($count): string { @@ -117,7 +121,6 @@ public function stream_read($count): string * Since we allowed the open to succeed, we should allow the close to occur * as well. * - * @return void */ public function stream_close(): void { @@ -160,7 +163,7 @@ public function stream_stat() public function url_stat($path, $flags) { if (self::$registeredStreamWrapperProtocols === null) { - throw new \PHPStan\ShouldNotHappenException(self::class . ' not registered: cannot operate. Do not call this method directly.'); + throw new ShouldNotHappenException(self::class . ' not registered: cannot operate. Do not call this method directly.'); } foreach (self::$registeredStreamWrapperProtocols as $protocol) { @@ -184,7 +187,6 @@ public function url_stat($path, $flags) /** * Simulates behavior of reading from an empty file. * - * @return bool */ public function stream_eof(): bool { @@ -204,7 +206,6 @@ public function stream_tell(): int /** * @param int $offset * @param int $whence - * @return bool */ public function stream_seek($offset, $whence): bool { @@ -234,7 +235,6 @@ public function stream_seek($offset, $whence): bool * @param int $option * @param int $arg1 * @param int $arg2 - * @return bool */ public function stream_set_option($option, $arg1, $arg2): bool { diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php index 9aaa11a6e4..8c21b5983b 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocator.php @@ -2,21 +2,29 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; +use PhpParser\Node; use PHPStan\BetterReflection\Identifier\Identifier; use PHPStan\BetterReflection\Identifier\IdentifierType; use PHPStan\BetterReflection\Reflection\Reflection; use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; +use PHPStan\Php\PhpVersion; +use PHPStan\ShouldNotHappenException; use function array_key_exists; +use function array_merge; +use function count; +use function php_strip_whitespace; +use function preg_match_all; +use function sprintf; +use function strtolower; class OptimizedDirectorySourceLocator implements SourceLocator { - private \PHPStan\Reflection\BetterReflection\SourceLocator\FileNodesFetcher $fileNodesFetcher; + private PhpFileCleaner $cleaner; - /** @var string[] */ - private array $files; + private string $extraTypes; /** @var array|null */ private ?array $classToFile = null; @@ -24,26 +32,31 @@ class OptimizedDirectorySourceLocator implements SourceLocator /** @var array>|null */ private ?array $functionToFiles = null; - /** @var array> */ + /** @var array> */ private array $classNodes = []; - /** @var array> */ + /** @var array> */ private array $functionNodes = []; - /** @var array */ - private array $locatedSourcesByFile = []; - /** - * @param FileNodesFetcher $fileNodesFetcher * @param string[] $files */ public function __construct( - FileNodesFetcher $fileNodesFetcher, - array $files + private FileNodesFetcher $fileNodesFetcher, + private PhpVersion $phpVersion, + private array $files, ) { - $this->fileNodesFetcher = $fileNodesFetcher; - $this->files = $files; + $extraTypes = ''; + $extraTypesArray = []; + if ($this->phpVersion->supportsEnums()) { + $extraTypes = '|enum'; + $extraTypesArray[] = 'enum'; + } + + $this->extraTypes = $extraTypes; + + $this->cleaner = new PhpFileCleaner(array_merge(['class', 'interface', 'trait', 'function'], $extraTypesArray)); } public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection @@ -60,8 +73,6 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): } $fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($file); - $locatedSource = $fetchedNodesResult->getLocatedSource(); - $this->locatedSourcesByFile[$file] = $locatedSource; foreach ($fetchedNodesResult->getClassNodes() as $identifierName => $fetchedClassNodes) { foreach ($fetchedClassNodes as $fetchedClassNode) { $this->classNodes[$identifierName] = $fetchedClassNode; @@ -85,8 +96,6 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): $files = $this->findFilesByFunction($functionName); foreach ($files as $file) { $fetchedNodesResult = $this->fileNodesFetcher->fetchNodes($file); - $locatedSource = $fetchedNodesResult->getLocatedSource(); - $this->locatedSourcesByFile[$file] = $locatedSource; foreach ($fetchedNodesResult->getFunctionNodes() as $identifierName => $fetchedFunctionNode) { $this->functionNodes[$identifierName] = $fetchedFunctionNode; } @@ -103,25 +112,17 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): } /** - * @param Reflector $reflector - * @param FetchedNode<\PhpParser\Node\Stmt\ClassLike>|FetchedNode<\PhpParser\Node\Stmt\Function_> $fetchedNode - * @return Reflection + * @param FetchedNode|FetchedNode $fetchedNode */ private function nodeToReflection(Reflector $reflector, FetchedNode $fetchedNode): Reflection { $nodeToReflection = new NodeToReflection(); - $reflection = $nodeToReflection->__invoke( + return $nodeToReflection->__invoke( $reflector, $fetchedNode->getNode(), - $this->locatedSourcesByFile[$fetchedNode->getFileName()], - $fetchedNode->getNamespace() + $fetchedNode->getLocatedSource(), + $fetchedNode->getNamespace(), ); - - if ($reflection === null) { - throw new \PHPStan\ShouldNotHappenException(); - } - - return $reflection; } private function findFileByClass(string $className): ?string @@ -129,7 +130,7 @@ private function findFileByClass(string $className): ?string if ($this->classToFile === null) { $this->init(); if ($this->classToFile === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } } @@ -141,7 +142,6 @@ private function findFileByClass(string $className): ?string } /** - * @param string $functionName * @return string[] */ private function findFilesByFunction(string $functionName): array @@ -149,7 +149,7 @@ private function findFilesByFunction(string $functionName): array if ($this->functionToFiles === null) { $this->init(); if ($this->functionToFiles === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } } @@ -187,7 +187,6 @@ private function init(): void * Inspired by Composer\Autoload\ClassMapGenerator::findClasses() * @link https://github.com/composer/composer/blob/45d3e133a4691eccb12e9cd6f9dfd76eddc1906d/src/Composer/Autoload/ClassMapGenerator.php#L216 * - * @param string $file * @return array{classes: string[], functions: string[]} */ private function findSymbols(string $file): array @@ -197,39 +196,19 @@ private function findSymbols(string $file): array return ['classes' => [], 'functions' => []]; } - if (!preg_match('{\b(?:class|interface|trait|function)\s}i', $contents)) { + $matchResults = (bool) preg_match_all(sprintf('{\b(?:class|interface|trait|function%s)\s}i', $this->extraTypes), $contents, $matches); + if (!$matchResults) { return ['classes' => [], 'functions' => []]; } - // strip heredocs/nowdocs - $contents = preg_replace('{<<<[ \t]*([\'"]?)(\w+)\\1(?:\r\n|\n|\r)(?:.*?)(?:\r\n|\n|\r)(?:\s*)\\2(?=\s+|[;,.)])}s', 'null', $contents); - // strip strings - $contents = preg_replace('{"[^"\\\\]*+(\\\\.[^"\\\\]*+)*+"|\'[^\'\\\\]*+(\\\\.[^\'\\\\]*+)*+\'}s', 'null', $contents); - // strip leading non-php code if needed - if (substr($contents, 0, 2) !== ' [], 'functions' => []]; - } - } - // strip non-php blocks in the file - $contents = preg_replace('{\?>(?:[^<]++|<(?!\?))*+<\?}s', '?>'); - if ($pos !== false && strpos(substr($contents, $pos), 'cleaner->clean($contents, count($matches[0])); - preg_match_all('{ + preg_match_all(sprintf('{ (?: - \b(?])(?Pclass|interface|trait|function) \s++ (?P&\s*)? (?P[a-zA-Z_\x7f-\xff:][a-zA-Z0-9_\x7f-\xff:\-]*+) + \b(?])(?Pclass|interface|trait|function%s) \s++ (?P&\s*)? (?P[a-zA-Z_\x7f-\xff:][a-zA-Z0-9_\x7f-\xff:\-]*+) | \b(?])(?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', $contents, $matches); + }ix', $this->extraTypes), $contents, $matches); $classes = []; $functions = []; @@ -246,9 +225,16 @@ private function findSymbols(string $file): array } $namespacedName = strtolower(ltrim($namespace . $name, '\\')); - if ($matches['type'][$i] === 'function') { + $lowerType = strtolower($matches['type'][$i]); + if ($lowerType === 'function') { $functions[] = $namespacedName; } else { + if ($lowerType === 'enum') { + $colonPos = strrpos($namespacedName, ':'); + if (false !== $colonPos) { + $namespacedName = substr($namespacedName, 0, $colonPos); + } + } $classes[] = $namespacedName; } } diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php index 71aee274ae..ec6d1e60db 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorFactory.php @@ -3,37 +3,33 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; use PHPStan\File\FileFinder; +use PHPStan\Php\PhpVersion; class OptimizedDirectorySourceLocatorFactory { - private FileNodesFetcher $fileNodesFetcher; - - private FileFinder $fileFinder; - - public function __construct(FileNodesFetcher $fileNodesFetcher, FileFinder $fileFinder) + public function __construct(private FileNodesFetcher $fileNodesFetcher, private FileFinder $fileFinder, private PhpVersion $phpVersion) { - $this->fileNodesFetcher = $fileNodesFetcher; - $this->fileFinder = $fileFinder; } public function createByDirectory(string $directory): OptimizedDirectorySourceLocator { return new OptimizedDirectorySourceLocator( $this->fileNodesFetcher, - $this->fileFinder->findFiles([$directory])->getFiles() + $this->phpVersion, + $this->fileFinder->findFiles([$directory])->getFiles(), ); } /** * @param string[] $files - * @return OptimizedDirectorySourceLocator */ public function createByFiles(array $files): OptimizedDirectorySourceLocator { return new OptimizedDirectorySourceLocator( $this->fileNodesFetcher, - $files + $this->phpVersion, + $files, ); } diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorRepository.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorRepository.php index 9f385a6d17..89c4ec0bcc 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorRepository.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorRepository.php @@ -2,17 +2,16 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; +use function array_key_exists; + class OptimizedDirectorySourceLocatorRepository { - private \PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedDirectorySourceLocatorFactory $factory; - /** @var array */ private array $locators = []; - public function __construct(OptimizedDirectorySourceLocatorFactory $factory) + public function __construct(private OptimizedDirectorySourceLocatorFactory $factory) { - $this->factory = $factory; } public function getOrCreate(string $directory): OptimizedDirectorySourceLocator diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php index b065a1f7ef..36bd379882 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedPsrAutoloaderLocator.php @@ -8,27 +8,22 @@ use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\BetterReflection\SourceLocator\Type\Composer\Psr\PsrAutoloaderMapping; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; +use function is_file; class OptimizedPsrAutoloaderLocator implements SourceLocator { - private PsrAutoloaderMapping $mapping; - - private \PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocatorRepository $optimizedSingleFileSourceLocatorRepository; - public function __construct( - PsrAutoloaderMapping $mapping, - OptimizedSingleFileSourceLocatorRepository $optimizedSingleFileSourceLocatorRepository + private PsrAutoloaderMapping $mapping, + private OptimizedSingleFileSourceLocatorRepository $optimizedSingleFileSourceLocatorRepository, ) { - $this->mapping = $mapping; - $this->optimizedSingleFileSourceLocatorRepository = $optimizedSingleFileSourceLocatorRepository; } public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection { foreach ($this->mapping->resolvePossibleFilePaths($identifier) as $file) { - if (!file_exists($file)) { + if (!is_file($file)) { continue; } @@ -44,7 +39,7 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): } /** - * @return Reflection[] + * @return array */ public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array { diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php index 25904df254..92e6c154c3 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocator.php @@ -12,23 +12,21 @@ use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; +use PHPStan\ShouldNotHappenException; +use function array_key_exists; +use function array_keys; +use function strtolower; class OptimizedSingleFileSourceLocator implements SourceLocator { - private \PHPStan\Reflection\BetterReflection\SourceLocator\FileNodesFetcher $fileNodesFetcher; - - private string $fileName; - - private ?\PHPStan\Reflection\BetterReflection\SourceLocator\FetchedNodesResult $fetchedNodesResult = null; + private ?FetchedNodesResult $fetchedNodesResult = null; public function __construct( - FileNodesFetcher $fileNodesFetcher, - string $fileName + private FileNodesFetcher $fileNodesFetcher, + private string $fileName, ) { - $this->fileNodesFetcher = $fileNodesFetcher; - $this->fileName = $fileName; } public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection @@ -48,11 +46,11 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): $classReflection = $nodeToReflection->__invoke( $reflector, $classNode->getNode(), - $this->fetchedNodesResult->getLocatedSource(), - $classNode->getNamespace() + $classNode->getLocatedSource(), + $classNode->getNamespace(), ); if (!$classReflection instanceof ReflectionClass) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $classReflection; @@ -69,11 +67,11 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): $functionReflection = $nodeToReflection->__invoke( $reflector, $functionNodes[$functionName]->getNode(), - $this->fetchedNodesResult->getLocatedSource(), - $functionNodes[$functionName]->getNamespace() + $functionNodes[$functionName]->getLocatedSource(), + $functionNodes[$functionName]->getNamespace(), ); if (!$functionReflection instanceof ReflectionFunction) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $functionReflection; @@ -86,14 +84,11 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): $constantReflection = $nodeToReflection->__invoke( $reflector, $stmtConst->getNode(), - $this->fetchedNodesResult->getLocatedSource(), - $stmtConst->getNamespace() + $stmtConst->getLocatedSource(), + $stmtConst->getNamespace(), ); - if ($constantReflection === null) { - continue; - } if (!$constantReflection instanceof ReflectionConstant) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ($constantReflection->getName() !== $identifier->getName()) { continue; @@ -106,15 +101,12 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): $constantReflection = $nodeToReflection->__invoke( $reflector, $stmtConst->getNode(), - $this->fetchedNodesResult->getLocatedSource(), + $stmtConst->getLocatedSource(), $stmtConst->getNamespace(), - $i + $i, ); - if ($constantReflection === null) { - continue; - } if (!$constantReflection instanceof ReflectionConstant) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ($constantReflection->getName() !== $identifier->getName()) { continue; @@ -127,7 +119,7 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): return null; } - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array diff --git a/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorRepository.php b/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorRepository.php index baaad91822..59e26df126 100644 --- a/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorRepository.php +++ b/src/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorRepository.php @@ -2,17 +2,16 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; +use function array_key_exists; + class OptimizedSingleFileSourceLocatorRepository { - private \PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocatorFactory $factory; - /** @var array */ private array $locators = []; - public function __construct(OptimizedSingleFileSourceLocatorFactory $factory) + public function __construct(private OptimizedSingleFileSourceLocatorFactory $factory) { - $this->factory = $factory; } public function getOrCreate(string $fileName): OptimizedSingleFileSourceLocator diff --git a/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php b/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php new file mode 100644 index 0000000000..715546bbb4 --- /dev/null +++ b/src/Reflection/BetterReflection/SourceLocator/PhpFileCleaner.php @@ -0,0 +1,223 @@ + + * @see https://github.com/composer/composer/pull/10107 + */ +class PhpFileCleaner +{ + + /** @var array */ + private array $typeConfig = []; + + private string $restPattern; + + private string $contents = ''; + + private int $len = 0; + + private int $index = 0; + + /** + * @param string[] $types + */ + public function __construct(array $types) + { + foreach ($types as $type) { + $this->typeConfig[$type[0]] = [ + 'name' => $type, + 'length' => strlen($type), + 'pattern' => '{.\b(?])' . $type . '\s++[a-zA-Z_\x7f-\xff:][a-zA-Z0-9_\x7f-\xff:\-]*+}Ais', + ]; + } + + $this->restPattern = '{[^?"\'typeConfig)) . ']+}A'; + } + + public function clean(string $contents, int $maxMatches): string + { + $this->contents = $contents; + $this->len = strlen($contents); + $this->index = 0; + + $clean = ''; + while ($this->index < $this->len) { + $this->skipToPhp(); + $clean .= 'index < $this->len) { + $char = $this->contents[$this->index]; + if ($char === '?' && $this->peek('>')) { + $clean .= '?>'; + $this->index += 2; + continue 2; + } + + if ($char === '"') { + $this->skipString('"'); + $clean .= 'null'; + continue; + } + + if ($char === "'") { + $this->skipString("'"); + $clean .= 'null'; + continue; + } + + if ($char === '<' && $this->peek('<') && $this->match('{<<<[ \t]*+([\'"]?)([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*+)\\1(?:\r\n|\n|\r)}A', $match)) { + $this->index += strlen($match[0]); + $this->skipHeredoc($match[2]); + $clean .= 'null'; + continue; + } + + if ($char === '/') { + if ($this->peek('/')) { + $this->skipToNewline(); + continue; + } + if ($this->peek('*')) { + $this->skipComment(); + } + } + + if ($maxMatches === 1 && isset($this->typeConfig[$char])) { + $type = $this->typeConfig[$char]; + if ( + substr($this->contents, $this->index, $type['length']) === $type['name'] + && preg_match($type['pattern'], $this->contents, $match, 0, $this->index - 1) + ) { + return $clean . $match[0]; + } + } + + $this->index += 1; + if ($this->match($this->restPattern, $match)) { + $clean .= $char . $match[0]; + $this->index += strlen($match[0]); + } else { + $clean .= $char; + } + } + } + + return $clean; + } + + private function skipToPhp(): void + { + while ($this->index < $this->len) { + if ($this->contents[$this->index] === '<' && $this->peek('?')) { + $this->index += 2; + break; + } + + $this->index += 1; + } + } + + private function skipString(string $delimiter): void + { + $this->index += 1; + while ($this->index < $this->len) { + if ($this->contents[$this->index] === '\\' && ($this->peek('\\') || $this->peek($delimiter))) { + $this->index += 2; + continue; + } + if ($this->contents[$this->index] === $delimiter) { + $this->index += 1; + break; + } + $this->index += 1; + } + } + + private function skipComment(): void + { + $this->index += 2; + while ($this->index < $this->len) { + if ($this->contents[$this->index] === '*' && $this->peek('/')) { + $this->index += 2; + break; + } + + $this->index += 1; + } + } + + private function skipToNewline(): void + { + while ($this->index < $this->len) { + if ($this->contents[$this->index] === "\r" || $this->contents[$this->index] === "\n") { + return; + } + $this->index += 1; + } + } + + private function skipHeredoc(string $delimiter): void + { + $firstDelimiterChar = $delimiter[0]; + $delimiterLength = strlen($delimiter); + $delimiterPattern = '{' . preg_quote($delimiter) . '(?![a-zA-Z0-9_\x80-\xff])}A'; + + while ($this->index < $this->len) { + // check if we find the delimiter after some spaces/tabs + switch ($this->contents[$this->index]) { + case "\t": + case ' ': + $this->index += 1; + continue 2; + case $firstDelimiterChar: + if ( + substr($this->contents, $this->index, $delimiterLength) === $delimiter + && $this->match($delimiterPattern) + ) { + $this->index += $delimiterLength; + return; + } + break; + } + + // skip the rest of the line + while ($this->index < $this->len) { + $this->skipToNewline(); + + // skip newlines + while ($this->index < $this->len && ($this->contents[$this->index] === "\r" || $this->contents[$this->index] === "\n")) { + $this->index += 1; + } + + break; + } + } + } + + private function peek(string $char): bool + { + return $this->index + 1 < $this->len && $this->contents[$this->index + 1] === $char; + } + + /** + * @param string[]|null $match + */ + private function match(string $regex, ?array &$match = null): bool + { + if (preg_match($regex, $this->contents, $match, 0, $this->index)) { + return true; + } + + return false; + } + +} diff --git a/src/Reflection/BetterReflection/SourceLocator/PhpVersionBlacklistSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/PhpVersionBlacklistSourceLocator.php index 9d9b7ec942..0731d733b0 100644 --- a/src/Reflection/BetterReflection/SourceLocator/PhpVersionBlacklistSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/PhpVersionBlacklistSourceLocator.php @@ -12,17 +12,11 @@ class PhpVersionBlacklistSourceLocator implements SourceLocator { - private SourceLocator $sourceLocator; - - private PhpStormStubsSourceStubber $phpStormStubsSourceStubber; - public function __construct( - SourceLocator $sourceLocator, - PhpStormStubsSourceStubber $phpStormStubsSourceStubber + private SourceLocator $sourceLocator, + private PhpStormStubsSourceStubber $phpStormStubsSourceStubber, ) { - $this->sourceLocator = $sourceLocator; - $this->phpStormStubsSourceStubber = $phpStormStubsSourceStubber; } public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection diff --git a/src/Reflection/BetterReflection/SourceLocator/SkipClassAliasSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/SkipClassAliasSourceLocator.php index 2afa00d549..fc8e931ffe 100644 --- a/src/Reflection/BetterReflection/SourceLocator/SkipClassAliasSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/SkipClassAliasSourceLocator.php @@ -7,15 +7,14 @@ use PHPStan\BetterReflection\Reflection\Reflection; use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; +use ReflectionClass; +use function class_exists; class SkipClassAliasSourceLocator implements SourceLocator { - private SourceLocator $sourceLocator; - - public function __construct(SourceLocator $sourceLocator) + public function __construct(private SourceLocator $sourceLocator) { - $this->sourceLocator = $sourceLocator; } public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection @@ -26,7 +25,10 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): return $this->sourceLocator->locateIdentifier($reflector, $identifier); } - $reflection = new \ReflectionClass($className); + $reflection = new ReflectionClass($className); + if ($reflection->getName() === 'ReturnTypeWillChange') { + return $this->sourceLocator->locateIdentifier($reflector, $identifier); + } if ($reflection->getFileName() === false) { return $this->sourceLocator->locateIdentifier($reflector, $identifier); } diff --git a/src/Reflection/BetterReflection/SourceStubber/Php8StubsSourceStubber.php b/src/Reflection/BetterReflection/SourceStubber/Php8StubsSourceStubber.php index c482ee70ef..d3a264c6db 100644 --- a/src/Reflection/BetterReflection/SourceStubber/Php8StubsSourceStubber.php +++ b/src/Reflection/BetterReflection/SourceStubber/Php8StubsSourceStubber.php @@ -6,6 +6,9 @@ use PHPStan\BetterReflection\SourceLocator\SourceStubber\StubData; use PHPStan\File\FileReader; use PHPStan\Php8StubsMap; +use function array_key_exists; +use function explode; +use function strtolower; class Php8StubsSourceStubber implements SourceStubber { diff --git a/src/Reflection/BetterReflection/SourceStubber/PhpStormStubsSourceStubberFactory.php b/src/Reflection/BetterReflection/SourceStubber/PhpStormStubsSourceStubberFactory.php new file mode 100644 index 0000000000..273dc62e3b --- /dev/null +++ b/src/Reflection/BetterReflection/SourceStubber/PhpStormStubsSourceStubberFactory.php @@ -0,0 +1,21 @@ +phpParser, $this->phpVersion->getVersionId()); + } + +} diff --git a/src/Reflection/BrokerAwareExtension.php b/src/Reflection/BrokerAwareExtension.php index 58bc2aa3bb..9ca104d127 100644 --- a/src/Reflection/BrokerAwareExtension.php +++ b/src/Reflection/BrokerAwareExtension.php @@ -4,7 +4,10 @@ use PHPStan\Broker\Broker; -/** @api */ +/** + * @api + * @deprecated Inject PHPStan\Reflection\ReflectionProvider into the constructor instead + */ interface BrokerAwareExtension { diff --git a/src/Reflection/ClassConstantReflection.php b/src/Reflection/ClassConstantReflection.php index cdebf0dc6b..6334ec6864 100644 --- a/src/Reflection/ClassConstantReflection.php +++ b/src/Reflection/ClassConstantReflection.php @@ -2,38 +2,30 @@ namespace PHPStan\Reflection; +use PHPStan\BetterReflection\NodeCompiler\Exception\UnableToCompileNode; +use PHPStan\Php\PhpVersion; use PHPStan\TrinaryLogic; use PHPStan\Type\ConstantTypeHelper; use PHPStan\Type\Type; +use ReflectionClassConstant; +use function method_exists; +use const NAN; class ClassConstantReflection implements ConstantReflection { - private \PHPStan\Reflection\ClassReflection $declaringClass; - - private \ReflectionClassConstant $reflection; - - private ?string $deprecatedDescription; - - private bool $isDeprecated; - - private bool $isInternal; - private ?Type $valueType = null; public function __construct( - ClassReflection $declaringClass, - \ReflectionClassConstant $reflection, - ?string $deprecatedDescription, - bool $isDeprecated, - bool $isInternal + private ClassReflection $declaringClass, + private ReflectionClassConstant $reflection, + private ?Type $phpDocType, + private PhpVersion $phpVersion, + private ?string $deprecatedDescription, + private bool $isDeprecated, + private bool $isInternal, ) { - $this->declaringClass = $declaringClass; - $this->reflection = $reflection; - $this->deprecatedDescription = $deprecatedDescription; - $this->isDeprecated = $isDeprecated; - $this->isInternal = $isInternal; } public function getName(): string @@ -43,12 +35,7 @@ public function getName(): string public function getFileName(): ?string { - $fileName = $this->declaringClass->getFileName(); - if ($fileName === false) { - return null; - } - - return $fileName; + return $this->declaringClass->getFileName(); } /** @@ -56,14 +43,28 @@ public function getFileName(): ?string */ public function getValue() { - return $this->reflection->getValue(); + try { + return $this->reflection->getValue(); + } catch (UnableToCompileNode) { + return NAN; + } + } + + public function hasPhpDocType(): bool + { + return $this->phpDocType !== null; } public function getValueType(): Type { if ($this->valueType === null) { - $this->valueType = ConstantTypeHelper::getTypeFromValue($this->getValue()); + if ($this->phpDocType === null) { + $this->valueType = ConstantTypeHelper::getTypeFromValue($this->getValue()); + } else { + $this->valueType = $this->phpDocType; + } } + return $this->valueType; } @@ -87,6 +88,19 @@ public function isPublic(): bool return $this->reflection->isPublic(); } + public function isFinal(): bool + { + if (method_exists($this->reflection, 'isFinal')) { + return $this->reflection->isFinal(); + } + + if (!$this->phpVersion->isInterfaceConstantImplicitlyFinal()) { + return false; + } + + return $this->declaringClass->isInterface(); + } + public function isDeprecated(): TrinaryLogic { return TrinaryLogic::createFromBoolean($this->isDeprecated); diff --git a/src/Reflection/ClassNameHelper.php b/src/Reflection/ClassNameHelper.php index 803129f14a..adf7905bfc 100644 --- a/src/Reflection/ClassNameHelper.php +++ b/src/Reflection/ClassNameHelper.php @@ -3,6 +3,7 @@ namespace PHPStan\Reflection; use Nette\Utils\Strings; +use function ltrim; class ClassNameHelper { diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index 2415737b5e..7be2d1651a 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -3,9 +3,10 @@ namespace PHPStan\Reflection; use Attribute; -use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass; use PHPStan\Php\PhpVersion; +use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\PhpDoc\ResolvedPhpDocBlock; +use PHPStan\PhpDoc\StubPhpDocProvider; use PHPStan\PhpDoc\Tag\ExtendsTag; use PHPStan\PhpDoc\Tag\ImplementsTag; use PHPStan\PhpDoc\Tag\MethodTag; @@ -16,6 +17,9 @@ use PHPStan\PhpDoc\Tag\TypeAliasTag; use PHPStan\Reflection\Php\PhpClassReflectionExtension; use PHPStan\Reflection\Php\PhpPropertyReflection; +use PHPStan\ShouldNotHappenException; +use PHPStan\Type\CircularTypeAliasDefinitionException; +use PHPStan\Type\ConstantTypeHelper; use PHPStan\Type\ErrorType; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\GenericObjectType; @@ -25,38 +29,42 @@ use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Type; use PHPStan\Type\TypeAlias; +use PHPStan\Type\TypehintHelper; use PHPStan\Type\VerbosityLevel; +use ReflectionClass; +use ReflectionEnum; +use ReflectionEnumBackedCase; +use ReflectionException; use ReflectionMethod; +use function array_diff; +use function array_filter; +use function array_key_exists; +use function array_map; +use function array_merge; +use function array_shift; +use function array_unique; +use function array_values; +use function count; +use function implode; +use function in_array; +use function is_bool; +use function is_file; +use function method_exists; +use function reset; +use function sprintf; +use function strtolower; /** @api */ -class ClassReflection implements ReflectionWithFilename +class ClassReflection { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - private PhpVersion $phpVersion; - - /** @var \PHPStan\Reflection\PropertiesClassReflectionExtension[] */ - private array $propertiesClassReflectionExtensions; - - /** @var \PHPStan\Reflection\MethodsClassReflectionExtension[] */ - private array $methodsClassReflectionExtensions; - - private string $displayName; - - private \ReflectionClass $reflection; - - private ?string $anonymousFilename; - - /** @var \PHPStan\Reflection\MethodReflection[] */ + /** @var MethodReflection[] */ private array $methods = []; - /** @var \PHPStan\Reflection\PropertyReflection[] */ + /** @var PropertyReflection[] */ private array $properties = []; - /** @var \PHPStan\Reflection\ConstantReflection[] */ + /** @var ConstantReflection[] */ private array $constants = []; /** @var int[]|null */ @@ -72,15 +80,9 @@ class ClassReflection implements ReflectionWithFilename private ?bool $isFinal = null; - /** @var ?TemplateTypeMap */ private ?TemplateTypeMap $templateTypeMap = null; - /** @var ?TemplateTypeMap */ - private ?TemplateTypeMap $resolvedTemplateTypeMap; - - private ?ResolvedPhpDocBlock $stubPhpDocBlock; - - private ?string $extraCacheKey; + private ?TemplateTypeMap $activeTemplateTypeMap = null; /** @var array|null */ private ?array $ancestors = null; @@ -90,17 +92,14 @@ class ClassReflection implements ReflectionWithFilename /** @var array */ private array $subclasses = []; - /** @var string|false|null */ - private $filename; + private string|false|null $filename = false; - /** @var string|false|null */ - private $reflectionDocComment; + private string|false|null $reflectionDocComment = false; - /** @var \PHPStan\Reflection\ClassReflection[]|null */ + /** @var ClassReflection[]|null */ private ?array $cachedInterfaces = null; - /** @var \PHPStan\Reflection\ClassReflection|false|null */ - private $cachedParentClass = null; + private ClassReflection|false|null $cachedParentClass = false; /** @var array|null */ private ?array $typeAliases = null; @@ -109,54 +108,35 @@ class ClassReflection implements ReflectionWithFilename private static array $resolvingTypeAliasImports = []; /** - * @param \PHPStan\Reflection\ReflectionProvider $reflectionProvider - * @param \PHPStan\Type\FileTypeMapper $fileTypeMapper - * @param \PHPStan\Reflection\PropertiesClassReflectionExtension[] $propertiesClassReflectionExtensions - * @param \PHPStan\Reflection\MethodsClassReflectionExtension[] $methodsClassReflectionExtensions - * @param string $displayName - * @param \ReflectionClass $reflection - * @param string|null $anonymousFilename - * @param ResolvedPhpDocBlock|null $stubPhpDocBlock - * @param string|null $extraCacheKey + * @param PropertiesClassReflectionExtension[] $propertiesClassReflectionExtensions + * @param MethodsClassReflectionExtension[] $methodsClassReflectionExtensions */ public function __construct( - ReflectionProvider $reflectionProvider, - FileTypeMapper $fileTypeMapper, - PhpVersion $phpVersion, - array $propertiesClassReflectionExtensions, - array $methodsClassReflectionExtensions, - string $displayName, - \ReflectionClass $reflection, - ?string $anonymousFilename, - ?TemplateTypeMap $resolvedTemplateTypeMap, - ?ResolvedPhpDocBlock $stubPhpDocBlock, - ?string $extraCacheKey = null + private ReflectionProvider $reflectionProvider, + private FileTypeMapper $fileTypeMapper, + private StubPhpDocProvider $stubPhpDocProvider, + private PhpDocInheritanceResolver $phpDocInheritanceResolver, + private PhpVersion $phpVersion, + private array $propertiesClassReflectionExtensions, + private array $methodsClassReflectionExtensions, + private string $displayName, + private ReflectionClass $reflection, + private ?string $anonymousFilename, + private ?TemplateTypeMap $resolvedTemplateTypeMap, + private ?ResolvedPhpDocBlock $stubPhpDocBlock, + private ?string $extraCacheKey = null, ) { - $this->reflectionProvider = $reflectionProvider; - $this->fileTypeMapper = $fileTypeMapper; - $this->phpVersion = $phpVersion; - $this->propertiesClassReflectionExtensions = $propertiesClassReflectionExtensions; - $this->methodsClassReflectionExtensions = $methodsClassReflectionExtensions; - $this->displayName = $displayName; - $this->reflection = $reflection; - $this->anonymousFilename = $anonymousFilename; - $this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap; - $this->stubPhpDocBlock = $stubPhpDocBlock; - $this->extraCacheKey = $extraCacheKey; } - public function getNativeReflection(): \ReflectionClass + public function getNativeReflection(): ReflectionClass { return $this->reflection; } - /** - * @return string|false - */ - public function getFileName() + public function getFileName(): ?string { - if (isset($this->filename)) { + if (!is_bool($this->filename)) { return $this->filename; } @@ -165,43 +145,34 @@ public function getFileName() } $fileName = $this->reflection->getFileName(); if ($fileName === false) { - return $this->filename = false; + return $this->filename = null; } - if (!file_exists($fileName)) { - return $this->filename = false; + if (!is_file($fileName)) { + return $this->filename = null; } return $this->filename = $fileName; } + /** + * @deprecated Use getFileName() + */ public function getFileNameWithPhpDocs(): ?string { - if ($this->stubPhpDocBlock !== null) { - return $this->stubPhpDocBlock->getFilename(); - } - - $filename = $this->getFileName(); - if ($filename === false) { - return null; - } - - return $filename; + return $this->getFileName(); } - /** - * @return false|\PHPStan\Reflection\ClassReflection - */ - public function getParentClass() + public function getParentClass(): ?ClassReflection { - if ($this->cachedParentClass !== null) { + if (!is_bool($this->cachedParentClass)) { return $this->cachedParentClass; } $parentClass = $this->reflection->getParentClass(); if ($parentClass === false) { - return $this->cachedParentClass = false; + return $this->cachedParentClass = null; } $extendsTag = $this->getFirstExtendsTag(); @@ -212,7 +183,8 @@ public function getParentClass() if ($this->isGeneric()) { $extendedType = TemplateTypeHelper::resolveTemplateTypes( $extendedType, - $this->getActiveTemplateTypeMap() + $this->getPossiblyIncompleteActiveTemplateTypeMap(), + true, ); } @@ -226,7 +198,7 @@ public function getParentClass() $parentReflection = $this->reflectionProvider->getClass($parentClass->getName()); if ($parentReflection->isGeneric()) { return $parentReflection->withTypes( - array_values($parentReflection->getTemplateTypeMap()->resolveToBounds()->getTypes()) + array_values($parentReflection->getTemplateTypeMap()->map(static fn (): Type => new ErrorType())->getTypes()), ); } @@ -255,9 +227,7 @@ public function getDisplayName(bool $withTemplateTypes = true): string return $name; } - return $name . '<' . implode(',', array_map(static function (Type $type): string { - return $type->describe(VerbosityLevel::typeOnly()); - }, $this->resolvedTemplateTypeMap->getTypes())) . '>'; + return $name . '<' . implode(',', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::typeOnly()), $this->getActiveTemplateTypeMap()->getTypes())) . '>'; } public function getCacheKey(): string @@ -270,9 +240,7 @@ public function getCacheKey(): string $cacheKey = $this->displayName; if ($this->resolvedTemplateTypeMap !== null) { - $cacheKey .= '<' . implode(',', array_map(static function (Type $type): string { - return $type->describe(VerbosityLevel::cache()); - }, $this->resolvedTemplateTypeMap->getTypes())) . '>'; + $cacheKey .= '<' . implode(',', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::cache()), $this->resolvedTemplateTypeMap->getTypes())) . '>'; } if ($this->extraCacheKey !== null) { @@ -336,10 +304,10 @@ public function getClassHierarchyDistances(): array } /** - * @param \ReflectionClass $class - * @return \ReflectionClass[] + * @param ReflectionClass $class + * @return ReflectionClass[] */ - private function collectTraits(\ReflectionClass $class): array + private function collectTraits(ReflectionClass $class): array { $traits = []; $traitsLeftToAnalyze = $class->getTraits(); @@ -364,6 +332,10 @@ private function collectTraits(\ReflectionClass $class): array public function hasProperty(string $propertyName): bool { + if ($this->isEnum()) { + return $this->hasNativeProperty($propertyName); + } + foreach ($this->propertiesClassReflectionExtensions as $extension) { if ($extension->hasProperty($this, $propertyName)) { return true; @@ -405,7 +377,7 @@ public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): } if (!isset($this->methods[$key])) { - throw new \PHPStan\Reflection\MissingMethodFromReflectionException($this->getName(), $methodName); + throw new MissingMethodFromReflectionException($this->getName(), $methodName); } return $this->methods[$key]; @@ -419,25 +391,11 @@ public function hasNativeMethod(string $methodName): bool public function getNativeMethod(string $methodName): MethodReflection { if (!$this->hasNativeMethod($methodName)) { - throw new \PHPStan\Reflection\MissingMethodFromReflectionException($this->getName(), $methodName); + throw new MissingMethodFromReflectionException($this->getName(), $methodName); } return $this->getPhpExtension()->getNativeMethod($this, $methodName); } - /** - * @deprecated Use ClassReflection::getNativeReflection() instead. - * @return MethodReflection[] - */ - public function getNativeMethods(): array - { - $methods = []; - foreach ($this->reflection->getMethods() as $method) { - $methods[] = $this->getNativeMethod($method->getName()); - } - - return $methods; - } - public function hasConstructor(): bool { return $this->findConstructor() !== null; @@ -447,7 +405,7 @@ public function getConstructor(): MethodReflection { $constructor = $this->findConstructor(); if ($constructor === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $this->getNativeMethod($constructor->getName()); } @@ -474,7 +432,7 @@ private function getPhpExtension(): PhpClassReflectionExtension { $extension = $this->methodsClassReflectionExtensions[0]; if (!$extension instanceof PhpClassReflectionExtension) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $extension; @@ -482,6 +440,10 @@ private function getPhpExtension(): PhpClassReflectionExtension public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection { + if ($this->isEnum()) { + return $this->getNativeProperty($propertyName); + } + $key = $propertyName; if ($scope->isInClass()) { $key = sprintf('%s-%s', $key, $scope->getClassReflection()->getCacheKey()); @@ -501,7 +463,7 @@ public function getProperty(string $propertyName, ClassMemberAccessAnswerer $sco } if (!isset($this->properties[$key])) { - throw new \PHPStan\Reflection\MissingPropertyFromReflectionException($this->getName(), $propertyName); + throw new MissingPropertyFromReflectionException($this->getName(), $propertyName); } return $this->properties[$key]; @@ -515,7 +477,7 @@ public function hasNativeProperty(string $propertyName): bool public function getNativeProperty(string $propertyName): PhpPropertyReflection { if (!$this->hasNativeProperty($propertyName)) { - throw new \PHPStan\Reflection\MissingPropertyFromReflectionException($this->getName(), $propertyName); + throw new MissingPropertyFromReflectionException($this->getName(), $propertyName); } return $this->getPhpExtension()->getNativeProperty($this, $propertyName); @@ -536,9 +498,100 @@ public function isTrait(): bool return $this->reflection->isTrait(); } + public function isEnum(): bool + { + if (method_exists($this->reflection, 'isEnum')) { + return $this->reflection->isEnum(); + } + + return false; + } + + public function isBackedEnum(): bool + { + if (!$this->reflection instanceof ReflectionEnum) { + return false; + } + + return $this->reflection->isBacked(); + } + + public function getBackedEnumType(): ?Type + { + if (!$this->reflection instanceof ReflectionEnum) { + return null; + } + + if (!$this->reflection->isBacked()) { + return null; + } + + $reflectionType = $this->reflection->getBackingType(); + if ($reflectionType === null) { + return null; + } + + return TypehintHelper::decideTypeFromReflection($reflectionType); + } + + public function hasEnumCase(string $name): bool + { + if (!$this->isEnum()) { + return false; + } + + if (!method_exists($this->reflection, 'hasCase')) { + return false; + } + + return $this->reflection->hasCase($name); + } + + /** + * @return array + */ + public function getEnumCases(): array + { + if (!$this->reflection instanceof ReflectionEnum) { + throw new ShouldNotHappenException(); + } + + $cases = []; + foreach ($this->reflection->getCases() as $case) { + $valueType = null; + if ($case instanceof ReflectionEnumBackedCase) { + $valueType = ConstantTypeHelper::getTypeFromValue($case->getBackingValue()); + } + /** @var string $caseName */ + $caseName = $case->getName(); + $cases[$caseName] = new EnumCaseReflection($this, $caseName, $valueType); + } + + return $cases; + } + + public function getEnumCase(string $name): EnumCaseReflection + { + if (!$this->hasEnumCase($name)) { + throw new ShouldNotHappenException(sprintf('Enum case %s::%s does not exist.', $this->getDisplayName(), $name)); + } + + if (!$this->reflection instanceof ReflectionEnum) { + throw new ShouldNotHappenException(); + } + + $case = $this->reflection->getCase($name); + $valueType = null; + if ($case instanceof ReflectionEnumBackedCase) { + $valueType = ConstantTypeHelper::getTypeFromValue($case->getBackingValue()); + } + + return new EnumCaseReflection($this, $name, $valueType); + } + public function isClass(): bool { - return !$this->isInterface() && !$this->isTrait(); + return !$this->isInterface() && !$this->isTrait() && !$this->isEnum(); } public function isAnonymous(): bool @@ -558,7 +611,7 @@ public function isSubclassOf(string $className): bool try { return $this->subclasses[$className] = $this->reflection->isSubclassOf($className); - } catch (\ReflectionException $e) { + } catch (ReflectionException) { return $this->subclasses[$className] = false; } } @@ -567,19 +620,19 @@ public function implementsInterface(string $className): bool { try { return $this->reflection->implementsInterface($className); - } catch (\ReflectionException $e) { + } catch (ReflectionException) { return false; } } /** - * @return \PHPStan\Reflection\ClassReflection[] + * @return ClassReflection[] */ public function getParents(): array { $parents = []; $parent = $this->getParentClass(); - while ($parent !== false) { + while ($parent !== null) { $parents[] = $parent; $parent = $parent->getParentClass(); } @@ -588,7 +641,7 @@ public function getParents(): array } /** - * @return \PHPStan\Reflection\ClassReflection[] + * @return ClassReflection[] */ public function getInterfaces(): array { @@ -596,81 +649,117 @@ public function getInterfaces(): array return $this->cachedInterfaces; } - $interfaces = []; - + $interfaces = $this->getImmediateInterfaces(); + $immediateInterfaces = $interfaces; $parent = $this->getParentClass(); - if ($parent !== false) { - foreach ($parent->getInterfaces() as $interface) { - $interfaces[$interface->getName()] = $interface; + while ($parent !== null) { + foreach ($parent->getImmediateInterfaces() as $parentInterface) { + $interfaces[$parentInterface->getName()] = $parentInterface; + foreach ($this->collectInterfaces($parentInterface) as $parentInterfaceInterface) { + $interfaces[$parentInterfaceInterface->getName()] = $parentInterfaceInterface; + } } - } - if ($this->reflection->isInterface()) { - $implementsTags = $this->getExtendsTags(); - } else { - $implementsTags = $this->getImplementsTags(); + $parent = $parent->getParentClass(); } - $interfaceNames = $this->reflection->getInterfaceNames(); - $genericInterfaces = []; + foreach ($immediateInterfaces as $immediateInterface) { + foreach ($this->collectInterfaces($immediateInterface) as $interfaceInterface) { + $interfaces[$interfaceInterface->getName()] = $interfaceInterface; + } + } - foreach ($implementsTags as $implementsTag) { - $implementedType = $implementsTag->getType(); + $this->cachedInterfaces = $interfaces; - if (!$this->isValidAncestorType($implementedType, $interfaceNames)) { - continue; - } + return $interfaces; + } - if ($this->isGeneric()) { - $implementedType = TemplateTypeHelper::resolveTemplateTypes( - $implementedType, - $this->getActiveTemplateTypeMap() - ); + /** + * @return ClassReflection[] + */ + private function collectInterfaces(ClassReflection $interface): array + { + $interfaces = []; + foreach ($interface->getImmediateInterfaces() as $immediateInterface) { + $interfaces[$immediateInterface->getName()] = $immediateInterface; + foreach ($this->collectInterfaces($immediateInterface) as $immediateInterfaceInterface) { + $interfaces[$immediateInterfaceInterface->getName()] = $immediateInterfaceInterface; } + } - if (!$implementedType instanceof GenericObjectType) { - continue; - } + return $interfaces; + } - $reflectionIface = $implementedType->getClassReflection(); - if ($reflectionIface === null) { - continue; + /** + * @return ClassReflection[] + */ + public function getImmediateInterfaces(): array + { + $indirectInterfaceNames = []; + $parent = $this->getParentClass(); + while ($parent !== null) { + foreach ($parent->getNativeReflection()->getInterfaceNames() as $parentInterfaceName) { + $indirectInterfaceNames[] = $parentInterfaceName; } - $genericInterfaces[] = $reflectionIface; + $parent = $parent->getParentClass(); } - foreach ($genericInterfaces as $genericInterface) { - $interfaces = array_merge($interfaces, $genericInterface->getInterfaces()); + foreach ($this->getNativeReflection()->getInterfaces() as $interfaceInterface) { + foreach ($interfaceInterface->getInterfaceNames() as $interfaceInterfaceName) { + $indirectInterfaceNames[] = $interfaceInterfaceName; + } } - foreach ($genericInterfaces as $genericInterface) { - $interfaces[$genericInterface->getName()] = $genericInterface; + if ($this->reflection->isInterface()) { + $implementsTags = $this->getExtendsTags(); + } else { + $implementsTags = $this->getImplementsTags(); } - foreach ($interfaceNames as $interfaceName) { - if (isset($interfaces[$interfaceName])) { + $immediateInterfaceNames = array_diff($this->getNativeReflection()->getInterfaceNames(), $indirectInterfaceNames); + $immediateInterfaces = []; + foreach ($immediateInterfaceNames as $immediateInterfaceName) { + if (!$this->reflectionProvider->hasClass($immediateInterfaceName)) { continue; } - $interfaceReflection = $this->reflectionProvider->getClass($interfaceName); - if (!$interfaceReflection->isGeneric()) { - $interfaces[$interfaceName] = $interfaceReflection; + $immediateInterface = $this->reflectionProvider->getClass($immediateInterfaceName); + if (array_key_exists($immediateInterface->getName(), $implementsTags)) { + $implementsTag = $implementsTags[$immediateInterface->getName()]; + $implementedType = $implementsTag->getType(); + if ($this->isGeneric()) { + $implementedType = TemplateTypeHelper::resolveTemplateTypes( + $implementedType, + $this->getPossiblyIncompleteActiveTemplateTypeMap(), + true, + ); + } + + if ( + $implementedType instanceof GenericObjectType + && $implementedType->getClassReflection() !== null + ) { + $immediateInterfaces[$immediateInterface->getName()] = $implementedType->getClassReflection(); + continue; + } + } + + if ($immediateInterface->isGeneric()) { + $immediateInterfaces[$immediateInterface->getName()] = $immediateInterface->withTypes( + array_values($immediateInterface->getTemplateTypeMap()->map(static fn (): Type => new ErrorType())->getTypes()), + ); continue; } - $interfaces[$interfaceName] = $interfaceReflection->withTypes( - array_values($interfaceReflection->getTemplateTypeMap()->resolveToBounds()->getTypes()) - ); + $immediateInterfaces[$immediateInterface->getName()] = $immediateInterface; } - $this->cachedInterfaces = $interfaces; - - return $interfaces; + return $immediateInterfaces; } /** - * @return array + * @return array */ public function getTraits(bool $recursive = false): array { @@ -684,9 +773,7 @@ public function getTraits(bool $recursive = false): array $traits = $this->getNativeReflection()->getTraits(); } - $traits = array_map(function (\ReflectionClass $trait): ClassReflection { - return $this->reflectionProvider->getClass($trait->getName()); - }, $traits); + $traits = array_map(fn (ReflectionClass $trait): ClassReflection => $this->reflectionProvider->getClass($trait->getName()), $traits); if ($recursive) { $parentClass = $this->getNativeReflection()->getParentClass(); @@ -694,7 +781,7 @@ public function getTraits(bool $recursive = false): array if ($parentClass !== false) { return array_merge( $traits, - $this->reflectionProvider->getClass($parentClass->getName())->getTraits(true) + $this->reflectionProvider->getClass($parentClass->getName())->getTraits(true), ); } } @@ -709,7 +796,7 @@ public function getParentClassesNames(): array { $parentNames = []; $currentClassReflection = $this; - while ($currentClassReflection->getParentClass() !== false) { + while ($currentClassReflection->getParentClass() !== null) { $parentNames[] = $currentClassReflection->getParentClass()->getName(); $currentClassReflection = $currentClassReflection->getParentClass(); } @@ -736,30 +823,50 @@ public function getConstant(string $name): ConstantReflection if (!isset($this->constants[$name])) { $reflectionConstant = $this->getNativeReflection()->getReflectionConstant($name); if ($reflectionConstant === false) { - throw new \PHPStan\Reflection\MissingConstantFromReflectionException($this->getName(), $name); + throw new MissingConstantFromReflectionException($this->getName(), $name); } $deprecatedDescription = null; $isDeprecated = false; $isInternal = false; - $declaringClass = $reflectionConstant->getDeclaringClass(); + $declaringClass = $this->reflectionProvider->getClass($reflectionConstant->getDeclaringClass()->getName()); $fileName = $declaringClass->getFileName(); - if ($reflectionConstant->getDocComment() !== false && $fileName !== false) { - $docComment = $reflectionConstant->getDocComment(); - $className = $declaringClass->getName(); - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc($fileName, $className, null, null, $docComment); + $phpDocType = null; + $resolvedPhpDoc = $this->stubPhpDocProvider->findClassConstantPhpDoc( + $declaringClass->getName(), + $name, + ); + if ($resolvedPhpDoc === null && $fileName !== null) { + $docComment = null; + if ($reflectionConstant->getDocComment() !== false) { + $docComment = $reflectionConstant->getDocComment(); + } + $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForConstant( + $docComment, + $declaringClass, + $fileName, + $name, + ); + } + if ($resolvedPhpDoc !== null) { $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null; $isDeprecated = $resolvedPhpDoc->isDeprecated(); $isInternal = $resolvedPhpDoc->isInternal(); + $varTags = $resolvedPhpDoc->getVarTags(); + if (isset($varTags[0]) && count($varTags) === 1) { + $phpDocType = $varTags[0]->getType(); + } } $this->constants[$name] = new ClassConstantReflection( - $this->reflectionProvider->getClass($declaringClass->getName()), + $declaringClass, $reflectionConstant, + $phpDocType, + $this->phpVersion, $deprecatedDescription, $isDeprecated, - $isInternal + $isInternal, ); } return $this->constants[$name]; @@ -801,7 +908,7 @@ public function getTypeAliases(): array // prevent circular imports if (array_key_exists($this->getName(), self::$resolvingTypeAliasImports)) { - throw new \PHPStan\Type\CircularTypeAliasDefinitionException(); + throw new CircularTypeAliasDefinitionException(); } self::$resolvingTypeAliasImports[$this->getName()] = true; @@ -818,7 +925,7 @@ public function getTypeAliases(): array try { $typeAliases = $importedFromReflection->getTypeAliases(); - } catch (\PHPStan\Type\CircularTypeAliasDefinitionException $e) { + } catch (CircularTypeAliasDefinitionException) { return TypeAlias::invalid(); } @@ -831,15 +938,11 @@ public function getTypeAliases(): array unset(self::$resolvingTypeAliasImports[$this->getName()]); - $localAliases = array_map(static function (TypeAliasTag $typeAliasTag): TypeAlias { - return $typeAliasTag->getTypeAlias(); - }, $typeAliasTags); + $localAliases = array_map(static fn (TypeAliasTag $typeAliasTag): TypeAlias => $typeAliasTag->getTypeAlias(), $typeAliasTags); $this->typeAliases = array_filter( array_merge($importedAliases, $localAliases), - static function (?TypeAlias $typeAlias): bool { - return $typeAlias !== null; - } + static fn (?TypeAlias $typeAlias): bool => $typeAlias !== null, ); } @@ -885,10 +988,13 @@ public function isInternal(): bool public function isFinal(): bool { + if ($this->isFinalByKeyword()) { + return true; + } + if ($this->isFinal === null) { $resolvedPhpDoc = $this->getResolvedPhpDoc(); - $this->isFinal = $this->reflection->isFinal() - || ($resolvedPhpDoc !== null && $resolvedPhpDoc->isFinal()); + $this->isFinal = $resolvedPhpDoc !== null && $resolvedPhpDoc->isFinal(); } return $this->isFinal; @@ -906,18 +1012,7 @@ public function isAttributeClass(): bool private function findAttributeClass(): ?Attribute { - if ($this->isInterface() || $this->isTrait()) { - return null; - } - - if ($this->reflection instanceof ReflectionClass) { - foreach ($this->reflection->getBetterReflection()->getAttributes() as $attribute) { - if ($attribute->getName() === \Attribute::class) { - /** @var \Attribute */ - return $attribute->newInstance(); - } - } - + if ($this->isInterface() || $this->isTrait() || $this->isEnum()) { return null; } @@ -925,7 +1020,7 @@ private function findAttributeClass(): ?Attribute return null; } - $nativeAttributes = $this->reflection->getAttributes(\Attribute::class); + $nativeAttributes = $this->reflection->getAttributes(Attribute::class); if (count($nativeAttributes) === 1) { /** @var Attribute */ return $nativeAttributes[0]->newInstance(); @@ -938,7 +1033,7 @@ public function getAttributeClassFlags(): int { $attribute = $this->findAttributeClass(); if ($attribute === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $attribute->flags; @@ -958,9 +1053,7 @@ public function getTemplateTypeMap(): TemplateTypeMap $templateTypeScope = TemplateTypeScope::createWithClass($this->getName()); - $templateTypeMap = new TemplateTypeMap(array_map(static function (TemplateTag $tag) use ($templateTypeScope): Type { - return TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag); - }, $this->getTemplateTags())); + $templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $this->getTemplateTags())); $this->templateTypeMap = $templateTypeMap; @@ -968,6 +1061,29 @@ public function getTemplateTypeMap(): TemplateTypeMap } public function getActiveTemplateTypeMap(): TemplateTypeMap + { + if ($this->activeTemplateTypeMap !== null) { + return $this->activeTemplateTypeMap; + } + $resolved = $this->resolvedTemplateTypeMap; + if ($resolved !== null) { + $templateTypeMap = $this->getTemplateTypeMap(); + return $this->activeTemplateTypeMap = $resolved->map(static function (string $name, Type $type) use ($templateTypeMap): Type { + if ($type instanceof ErrorType) { + $templateType = $templateTypeMap->getType($name); + if ($templateType !== null) { + return TemplateTypeHelper::resolveToBounds($templateType); + } + } + + return $type; + }); + } + + return $this->activeTemplateTypeMap = $this->getTemplateTypeMap(); + } + + public function getPossiblyIncompleteActiveTemplateTypeMap(): TemplateTypeMap { return $this->resolvedTemplateTypeMap ?? $this->getTemplateTypeMap(); } @@ -975,6 +1091,10 @@ public function getActiveTemplateTypeMap(): TemplateTypeMap public function isGeneric(): bool { if ($this->isGeneric === null) { + if ($this->isEnum()) { + return $this->isGeneric = false; + } + $this->isGeneric = count($this->getTemplateTags()) > 0; } @@ -983,7 +1103,6 @@ public function isGeneric(): bool /** * @param array $types - * @return \PHPStan\Type\Generic\TemplateTypeMap */ public function typeMapFromList(array $types): TemplateTypeMap { @@ -1026,6 +1145,8 @@ public function withTypes(array $types): self return new self( $this->reflectionProvider, $this->fileTypeMapper, + $this->stubPhpDocProvider, + $this->phpDocInheritanceResolver, $this->phpVersion, $this->propertiesClassReflectionExtensions, $this->methodsClassReflectionExtensions, @@ -1033,7 +1154,7 @@ public function withTypes(array $types): self $this->reflection, $this->anonymousFilename, $this->typeMapFromList($types), - $this->stubPhpDocBlock + $this->stubPhpDocBlock, ); } @@ -1044,15 +1165,16 @@ public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock } $fileName = $this->getFileName(); - if ($fileName === false) { + if ($fileName === null) { return null; } - if ($this->reflectionDocComment === null) { - $this->reflectionDocComment = $this->reflection->getDocComment(); + if (is_bool($this->reflectionDocComment)) { + $docComment = $this->reflection->getDocComment(); + $this->reflectionDocComment = $docComment !== false ? $docComment : null; } - if ($this->reflectionDocComment === false) { + if ($this->reflectionDocComment === null) { return null; } @@ -1068,8 +1190,8 @@ private function getFirstExtendsTag(): ?ExtendsTag return null; } - /** @return ExtendsTag[] */ - private function getExtendsTags(): array + /** @return array */ + public function getExtendsTags(): array { $resolvedPhpDoc = $this->getResolvedPhpDoc(); if ($resolvedPhpDoc === null) { @@ -1079,8 +1201,8 @@ private function getExtendsTags(): array return $resolvedPhpDoc->getExtendsTags(); } - /** @return ImplementsTag[] */ - private function getImplementsTags(): array + /** @return array */ + public function getImplementsTags(): array { $resolvedPhpDoc = $this->getResolvedPhpDoc(); if ($resolvedPhpDoc === null) { @@ -1136,7 +1258,7 @@ public function getAncestors(): array } $parent = $this->getParentClass(); - if ($parent !== false) { + if ($parent !== null) { $addToAncestors($parent->getName(), $parent); foreach ($parent->getAncestors() as $name => $ancestor) { $addToAncestors($name, $ancestor); @@ -1224,7 +1346,7 @@ public function getResolvedMixinTypes(): array $types[] = TemplateTypeHelper::resolveTemplateTypes( $mixinTag->getType(), - $this->getActiveTemplateTypeMap() + $this->getActiveTemplateTypeMap(), ); } diff --git a/src/Reflection/ClassReflectionExtensionRegistry.php b/src/Reflection/ClassReflectionExtensionRegistry.php index 9acc50ae61..e18880e0be 100644 --- a/src/Reflection/ClassReflectionExtensionRegistry.php +++ b/src/Reflection/ClassReflectionExtensionRegistry.php @@ -3,25 +3,19 @@ namespace PHPStan\Reflection; use PHPStan\Broker\Broker; +use function array_merge; class ClassReflectionExtensionRegistry { - /** @var \PHPStan\Reflection\PropertiesClassReflectionExtension[] */ - private array $propertiesClassReflectionExtensions; - - /** @var \PHPStan\Reflection\MethodsClassReflectionExtension[] */ - private array $methodsClassReflectionExtensions; - /** - * @param \PHPStan\Broker\Broker $broker - * @param \PHPStan\Reflection\PropertiesClassReflectionExtension[] $propertiesClassReflectionExtensions - * @param \PHPStan\Reflection\MethodsClassReflectionExtension[] $methodsClassReflectionExtensions + * @param PropertiesClassReflectionExtension[] $propertiesClassReflectionExtensions + * @param MethodsClassReflectionExtension[] $methodsClassReflectionExtensions */ public function __construct( Broker $broker, - array $propertiesClassReflectionExtensions, - array $methodsClassReflectionExtensions + private array $propertiesClassReflectionExtensions, + private array $methodsClassReflectionExtensions, ) { foreach (array_merge($propertiesClassReflectionExtensions, $methodsClassReflectionExtensions) as $extension) { @@ -31,12 +25,10 @@ public function __construct( $extension->setBroker($broker); } - $this->propertiesClassReflectionExtensions = $propertiesClassReflectionExtensions; - $this->methodsClassReflectionExtensions = $methodsClassReflectionExtensions; } /** - * @return \PHPStan\Reflection\PropertiesClassReflectionExtension[] + * @return PropertiesClassReflectionExtension[] */ public function getPropertiesClassReflectionExtensions(): array { @@ -44,7 +36,7 @@ public function getPropertiesClassReflectionExtensions(): array } /** - * @return \PHPStan\Reflection\MethodsClassReflectionExtension[] + * @return MethodsClassReflectionExtension[] */ public function getMethodsClassReflectionExtensions(): array { diff --git a/src/Reflection/Constant/RuntimeConstantReflection.php b/src/Reflection/Constant/RuntimeConstantReflection.php index a703fa4a34..341ae68cc4 100644 --- a/src/Reflection/Constant/RuntimeConstantReflection.php +++ b/src/Reflection/Constant/RuntimeConstantReflection.php @@ -9,21 +9,12 @@ class RuntimeConstantReflection implements GlobalConstantReflection { - private string $name; - - private Type $valueType; - - private ?string $fileName; - public function __construct( - string $name, - Type $valueType, - ?string $fileName + private string $name, + private Type $valueType, + private ?string $fileName, ) { - $this->name = $name; - $this->valueType = $valueType; - $this->fileName = $fileName; } public function getName(): string diff --git a/src/Reflection/Dummy/ChangedTypeMethodReflection.php b/src/Reflection/Dummy/ChangedTypeMethodReflection.php index 7c65952773..4ccba18790 100644 --- a/src/Reflection/Dummy/ChangedTypeMethodReflection.php +++ b/src/Reflection/Dummy/ChangedTypeMethodReflection.php @@ -12,22 +12,11 @@ class ChangedTypeMethodReflection implements MethodReflection { - private ClassReflection $declaringClass; - - private MethodReflection $reflection; - - /** @var ParametersAcceptor[] */ - private array $variants; - /** - * @param MethodReflection $reflection * @param ParametersAcceptor[] $variants */ - public function __construct(ClassReflection $declaringClass, MethodReflection $reflection, array $variants) + public function __construct(private ClassReflection $declaringClass, private MethodReflection $reflection, private array $variants) { - $this->declaringClass = $declaringClass; - $this->reflection = $reflection; - $this->variants = $variants; } public function getDeclaringClass(): ClassReflection diff --git a/src/Reflection/Dummy/ChangedTypePropertyReflection.php b/src/Reflection/Dummy/ChangedTypePropertyReflection.php index e8217e91f4..018d8593fa 100644 --- a/src/Reflection/Dummy/ChangedTypePropertyReflection.php +++ b/src/Reflection/Dummy/ChangedTypePropertyReflection.php @@ -11,20 +11,8 @@ class ChangedTypePropertyReflection implements WrapperPropertyReflection { - private ClassReflection $declaringClass; - - private PropertyReflection $reflection; - - private Type $readableType; - - private Type $writableType; - - public function __construct(ClassReflection $declaringClass, PropertyReflection $reflection, Type $readableType, Type $writableType) + public function __construct(private ClassReflection $declaringClass, private PropertyReflection $reflection, private Type $readableType, private Type $writableType) { - $this->declaringClass = $declaringClass; - $this->reflection = $reflection; - $this->readableType = $readableType; - $this->writableType = $writableType; } public function getDeclaringClass(): ClassReflection diff --git a/src/Reflection/Dummy/DummyConstantReflection.php b/src/Reflection/Dummy/DummyConstantReflection.php index 2137837e39..2b908355e5 100644 --- a/src/Reflection/Dummy/DummyConstantReflection.php +++ b/src/Reflection/Dummy/DummyConstantReflection.php @@ -2,28 +2,26 @@ namespace PHPStan\Reflection\Dummy; -use PHPStan\Broker\Broker; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ConstantReflection; +use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; use PHPStan\Type\Type; +use stdClass; class DummyConstantReflection implements ConstantReflection { - private string $name; - - public function __construct(string $name) + public function __construct(private string $name) { - $this->name = $name; } public function getDeclaringClass(): ClassReflection { - $broker = Broker::getInstance(); + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - return $broker->getClass(\stdClass::class); + return $reflectionProvider->getClass(stdClass::class); } public function getFileName(): ?string @@ -57,7 +55,7 @@ public function getName(): string public function getValue() { // so that Scope::getTypeFromValue() returns mixed - return new \stdClass(); + return new stdClass(); } public function getValueType(): Type diff --git a/src/Reflection/Dummy/DummyConstructorReflection.php b/src/Reflection/Dummy/DummyConstructorReflection.php index 18b75b19f4..1bcd01f6af 100644 --- a/src/Reflection/Dummy/DummyConstructorReflection.php +++ b/src/Reflection/Dummy/DummyConstructorReflection.php @@ -14,11 +14,8 @@ class DummyConstructorReflection implements MethodReflection { - private ClassReflection $declaringClass; - - public function __construct(ClassReflection $declaringClass) + public function __construct(private ClassReflection $declaringClass) { - $this->declaringClass = $declaringClass; } public function getDeclaringClass(): ClassReflection @@ -59,7 +56,7 @@ public function getVariants(): array null, [], false, - new VoidType() + new VoidType(), ), ]; } diff --git a/src/Reflection/Dummy/DummyMethodReflection.php b/src/Reflection/Dummy/DummyMethodReflection.php index bf12ffb38b..77b06eb5ae 100644 --- a/src/Reflection/Dummy/DummyMethodReflection.php +++ b/src/Reflection/Dummy/DummyMethodReflection.php @@ -2,29 +2,28 @@ namespace PHPStan\Reflection\Dummy; -use PHPStan\Broker\Broker; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; +use stdClass; class DummyMethodReflection implements MethodReflection { - private string $name; - - public function __construct(string $name) + public function __construct(private string $name) { - $this->name = $name; } public function getDeclaringClass(): ClassReflection { - $broker = Broker::getInstance(); + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - return $broker->getClass(\stdClass::class); + return $reflectionProvider->getClass(stdClass::class); } public function isStatic(): bool @@ -53,7 +52,7 @@ public function getPrototype(): ClassMemberReflection } /** - * @return \PHPStan\Reflection\ParametersAcceptor[] + * @return ParametersAcceptor[] */ public function getVariants(): array { diff --git a/src/Reflection/Dummy/DummyPropertyReflection.php b/src/Reflection/Dummy/DummyPropertyReflection.php index 1f5b97379e..b72a011960 100644 --- a/src/Reflection/Dummy/DummyPropertyReflection.php +++ b/src/Reflection/Dummy/DummyPropertyReflection.php @@ -2,21 +2,22 @@ namespace PHPStan\Reflection\Dummy; -use PHPStan\Broker\Broker; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; use PHPStan\Type\Type; +use stdClass; class DummyPropertyReflection implements PropertyReflection { public function getDeclaringClass(): ClassReflection { - $broker = Broker::getInstance(); + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - return $broker->getClass(\stdClass::class); + return $reflectionProvider->getClass(stdClass::class); } public function isStatic(): bool diff --git a/src/Reflection/EnumCaseReflection.php b/src/Reflection/EnumCaseReflection.php new file mode 100644 index 0000000000..b538f44b3f --- /dev/null +++ b/src/Reflection/EnumCaseReflection.php @@ -0,0 +1,30 @@ +declaringEnum; + } + + public function getName(): string + { + return $this->name; + } + + public function getBackingValueType(): ?Type + { + return $this->backingValueType; + } + +} diff --git a/src/Reflection/FunctionReflection.php b/src/Reflection/FunctionReflection.php index 6520745366..8ef8a806b2 100644 --- a/src/Reflection/FunctionReflection.php +++ b/src/Reflection/FunctionReflection.php @@ -11,8 +11,10 @@ interface FunctionReflection public function getName(): string; + public function getFileName(): ?string; + /** - * @return \PHPStan\Reflection\ParametersAcceptor[] + * @return ParametersAcceptor[] */ public function getVariants(): array; diff --git a/src/Reflection/FunctionReflectionFactory.php b/src/Reflection/FunctionReflectionFactory.php index caf4d05e72..f2a2f7e355 100644 --- a/src/Reflection/FunctionReflectionFactory.php +++ b/src/Reflection/FunctionReflectionFactory.php @@ -5,26 +5,16 @@ use PHPStan\Reflection\Php\PhpFunctionReflection; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Type; +use ReflectionFunction; interface FunctionReflectionFactory { /** - * @param \ReflectionFunction $reflection - * @param TemplateTypeMap $templateTypeMap - * @param \PHPStan\Type\Type[] $phpDocParameterTypes - * @param Type|null $phpDocReturnType - * @param Type|null $phpDocThrowType - * @param string|null $deprecatedDescription - * @param bool $isDeprecated - * @param bool $isInternal - * @param bool $isFinal - * @param string|false $filename - * @param bool|null $isPure - * @return PhpFunctionReflection + * @param Type[] $phpDocParameterTypes */ public function create( - \ReflectionFunction $reflection, + ReflectionFunction $reflection, TemplateTypeMap $templateTypeMap, array $phpDocParameterTypes, ?Type $phpDocReturnType, @@ -33,8 +23,8 @@ public function create( bool $isDeprecated, bool $isInternal, bool $isFinal, - $filename, - ?bool $isPure = null + ?string $filename, + ?bool $isPure = null, ): PhpFunctionReflection; } diff --git a/src/Reflection/FunctionVariant.php b/src/Reflection/FunctionVariant.php index 9a354bbbb2..9936ae9244 100644 --- a/src/Reflection/FunctionVariant.php +++ b/src/Reflection/FunctionVariant.php @@ -9,36 +9,18 @@ class FunctionVariant implements ParametersAcceptor { - private TemplateTypeMap $templateTypeMap; - - private ?TemplateTypeMap $resolvedTemplateTypeMap; - - /** @var array */ - private array $parameters; - - private bool $isVariadic; - - private Type $returnType; - /** * @api * @param array $parameters - * @param bool $isVariadic - * @param Type $returnType */ public function __construct( - TemplateTypeMap $templateTypeMap, - ?TemplateTypeMap $resolvedTemplateTypeMap, - array $parameters, - bool $isVariadic, - Type $returnType + private TemplateTypeMap $templateTypeMap, + private ?TemplateTypeMap $resolvedTemplateTypeMap, + private array $parameters, + private bool $isVariadic, + private Type $returnType, ) { - $this->templateTypeMap = $templateTypeMap; - $this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap; - $this->parameters = $parameters; - $this->isVariadic = $isVariadic; - $this->returnType = $returnType; } public function getTemplateTypeMap(): TemplateTypeMap diff --git a/src/Reflection/FunctionVariantWithPhpDocs.php b/src/Reflection/FunctionVariantWithPhpDocs.php index 0a9554f394..aae15cb864 100644 --- a/src/Reflection/FunctionVariantWithPhpDocs.php +++ b/src/Reflection/FunctionVariantWithPhpDocs.php @@ -9,18 +9,9 @@ class FunctionVariantWithPhpDocs extends FunctionVariant implements ParametersAcceptorWithPhpDocs { - private Type $phpDocReturnType; - - private Type $nativeReturnType; - /** * @api - * @param TemplateTypeMap $templateTypeMap - * @param array $parameters - * @param bool $isVariadic - * @param Type $returnType - * @param Type $phpDocReturnType - * @param Type $nativeReturnType + * @param array $parameters */ public function __construct( TemplateTypeMap $templateTypeMap, @@ -28,8 +19,8 @@ public function __construct( array $parameters, bool $isVariadic, Type $returnType, - Type $phpDocReturnType, - Type $nativeReturnType + private Type $phpDocReturnType, + private Type $nativeReturnType, ) { parent::__construct( @@ -37,18 +28,16 @@ public function __construct( $resolvedTemplateTypeMap, $parameters, $isVariadic, - $returnType + $returnType, ); - $this->phpDocReturnType = $phpDocReturnType; - $this->nativeReturnType = $nativeReturnType; } /** - * @return array + * @return array */ public function getParameters(): array { - /** @var \PHPStan\Reflection\ParameterReflectionWithPhpDocs[] $parameters */ + /** @var ParameterReflectionWithPhpDocs[] $parameters */ $parameters = parent::getParameters(); return $parameters; diff --git a/src/Reflection/Generic/ResolvedFunctionVariant.php b/src/Reflection/Generic/ResolvedFunctionVariant.php deleted file mode 100644 index 81c76bc77f..0000000000 --- a/src/Reflection/Generic/ResolvedFunctionVariant.php +++ /dev/null @@ -1,89 +0,0 @@ -parametersAcceptor = $parametersAcceptor; - $this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap; - } - - public function getTemplateTypeMap(): TemplateTypeMap - { - return $this->parametersAcceptor->getTemplateTypeMap(); - } - - public function getResolvedTemplateTypeMap(): TemplateTypeMap - { - return $this->resolvedTemplateTypeMap; - } - - public function getParameters(): array - { - $parameters = $this->parameters; - - if ($parameters === null) { - $parameters = array_map(function (ParameterReflection $param): ParameterReflection { - return new DummyParameter( - $param->getName(), - TemplateTypeHelper::resolveTemplateTypes($param->getType(), $this->resolvedTemplateTypeMap), - $param->isOptional(), - $param->passedByReference(), - $param->isVariadic(), - $param->getDefaultValue() - ); - }, $this->parametersAcceptor->getParameters()); - - $this->parameters = $parameters; - } - - return $parameters; - } - - public function isVariadic(): bool - { - return $this->parametersAcceptor->isVariadic(); - } - - public function getReturnType(): Type - { - $type = $this->returnType; - - if ($type === null) { - $type = TemplateTypeHelper::resolveTemplateTypes( - $this->parametersAcceptor->getReturnType(), - $this->resolvedTemplateTypeMap - ); - - $this->returnType = $type; - } - - return $type; - } - -} diff --git a/src/Reflection/GenericParametersAcceptorResolver.php b/src/Reflection/GenericParametersAcceptorResolver.php index 34c56baf84..2e79b78c9b 100644 --- a/src/Reflection/GenericParametersAcceptorResolver.php +++ b/src/Reflection/GenericParametersAcceptorResolver.php @@ -5,13 +5,14 @@ use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Type; +use function array_merge; class GenericParametersAcceptorResolver { /** * @api - * @param \PHPStan\Type\Type[] $argTypes + * @param Type[] $argTypes */ public static function resolve(array $argTypes, ParametersAcceptor $parametersAcceptor): ParametersAcceptor { @@ -27,19 +28,15 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc } $paramType = $param->getType(); - - // todo zde "doaplikovat" typy, ktere se dosud nevyskytly - typicky callable(T) - parametr callable $typeMap = $typeMap->union($paramType->inferTemplateTypes($argType)); } return new ResolvedFunctionVariant( $parametersAcceptor, new TemplateTypeMap(array_merge( - $parametersAcceptor->getTemplateTypeMap()->map(static function (string $name, Type $type): Type { - return new ErrorType(); - })->getTypes(), - $typeMap->getTypes() - )) + $parametersAcceptor->getTemplateTypeMap()->map(static fn (string $name, Type $type): Type => new ErrorType())->getTypes(), + $typeMap->getTypes(), + )), ); } diff --git a/src/Reflection/InaccessibleMethod.php b/src/Reflection/InaccessibleMethod.php index 26b8e11cbd..f089c132ad 100644 --- a/src/Reflection/InaccessibleMethod.php +++ b/src/Reflection/InaccessibleMethod.php @@ -9,11 +9,8 @@ class InaccessibleMethod implements ParametersAcceptor { - private MethodReflection $methodReflection; - - public function __construct(MethodReflection $methodReflection) + public function __construct(private MethodReflection $methodReflection) { - $this->methodReflection = $methodReflection; } public function getMethod(): MethodReflection @@ -32,7 +29,7 @@ public function getResolvedTemplateTypeMap(): TemplateTypeMap } /** - * @return array + * @return array */ public function getParameters(): array { diff --git a/src/Reflection/MethodPrototypeReflection.php b/src/Reflection/MethodPrototypeReflection.php index cb2ecabc58..648f161df0 100644 --- a/src/Reflection/MethodPrototypeReflection.php +++ b/src/Reflection/MethodPrototypeReflection.php @@ -2,55 +2,26 @@ namespace PHPStan\Reflection; +use PHPStan\Type\Type; + class MethodPrototypeReflection implements ClassMemberReflection { - private \PHPStan\Reflection\ClassReflection $declaringClass; - - private string $name; - - private bool $isStatic; - - private bool $isPrivate; - - private bool $isPublic; - - private bool $isAbstract; - - private bool $isFinal; - - /** @var ParametersAcceptor[] */ - private array $variants; - /** - * @param string $name - * @param ClassReflection $declaringClass - * @param bool $isStatic - * @param bool $isPrivate - * @param bool $isPublic - * @param bool $isAbstract - * @param bool $isFinal * @param ParametersAcceptor[] $variants */ public function __construct( - string $name, - ClassReflection $declaringClass, - bool $isStatic, - bool $isPrivate, - bool $isPublic, - bool $isAbstract, - bool $isFinal, - array $variants + private string $name, + private ClassReflection $declaringClass, + private bool $isStatic, + private bool $isPrivate, + private bool $isPublic, + private bool $isAbstract, + private bool $isFinal, + private array $variants, + private ?Type $tentativeReturnType, ) { - $this->name = $name; - $this->declaringClass = $declaringClass; - $this->isStatic = $isStatic; - $this->isPrivate = $isPrivate; - $this->isPublic = $isPublic; - $this->isAbstract = $isAbstract; - $this->isFinal = $isFinal; - $this->variants = $variants; } public function getName(): string @@ -101,4 +72,9 @@ public function getVariants(): array return $this->variants; } + public function getTentativeReturnType(): ?Type + { + return $this->tentativeReturnType; + } + } diff --git a/src/Reflection/MethodReflection.php b/src/Reflection/MethodReflection.php index 18443d371b..8d601e9471 100644 --- a/src/Reflection/MethodReflection.php +++ b/src/Reflection/MethodReflection.php @@ -14,7 +14,7 @@ public function getName(): string; public function getPrototype(): ClassMemberReflection; /** - * @return \PHPStan\Reflection\ParametersAcceptor[] + * @return ParametersAcceptor[] */ public function getVariants(): array; diff --git a/src/Reflection/MissingConstantFromReflectionException.php b/src/Reflection/MissingConstantFromReflectionException.php index 7535b78fa7..230ba5902d 100644 --- a/src/Reflection/MissingConstantFromReflectionException.php +++ b/src/Reflection/MissingConstantFromReflectionException.php @@ -2,20 +2,23 @@ namespace PHPStan\Reflection; -class MissingConstantFromReflectionException extends \Exception +use Exception; +use function sprintf; + +class MissingConstantFromReflectionException extends Exception { public function __construct( string $className, - string $constantName + string $constantName, ) { parent::__construct( sprintf( 'Constant %s was not found in reflection of class %s.', $constantName, - $className - ) + $className, + ), ); } diff --git a/src/Reflection/MissingMethodFromReflectionException.php b/src/Reflection/MissingMethodFromReflectionException.php index 30f725c49a..49c7778cd6 100644 --- a/src/Reflection/MissingMethodFromReflectionException.php +++ b/src/Reflection/MissingMethodFromReflectionException.php @@ -2,20 +2,23 @@ namespace PHPStan\Reflection; -class MissingMethodFromReflectionException extends \Exception +use Exception; +use function sprintf; + +class MissingMethodFromReflectionException extends Exception { public function __construct( string $className, - string $methodName + string $methodName, ) { parent::__construct( sprintf( 'Method %s() was not found in reflection of class %s.', $methodName, - $className - ) + $className, + ), ); } diff --git a/src/Reflection/MissingPropertyFromReflectionException.php b/src/Reflection/MissingPropertyFromReflectionException.php index af67614d52..4d62565c1f 100644 --- a/src/Reflection/MissingPropertyFromReflectionException.php +++ b/src/Reflection/MissingPropertyFromReflectionException.php @@ -2,20 +2,23 @@ namespace PHPStan\Reflection; -class MissingPropertyFromReflectionException extends \Exception +use Exception; +use function sprintf; + +class MissingPropertyFromReflectionException extends Exception { public function __construct( string $className, - string $propertyName + string $propertyName, ) { parent::__construct( sprintf( 'Property $%s was not found in reflection of class %s.', $propertyName, - $className - ) + $className, + ), ); } diff --git a/src/Reflection/Mixin/MixinMethodReflection.php b/src/Reflection/Mixin/MixinMethodReflection.php index 544e6fc5f7..745a8cae15 100644 --- a/src/Reflection/Mixin/MixinMethodReflection.php +++ b/src/Reflection/Mixin/MixinMethodReflection.php @@ -11,14 +11,8 @@ class MixinMethodReflection implements MethodReflection { - private MethodReflection $reflection; - - private bool $static; - - public function __construct(MethodReflection $reflection, bool $static) + public function __construct(private MethodReflection $reflection, private bool $static) { - $this->reflection = $reflection; - $this->static = $static; } public function getDeclaringClass(): ClassReflection diff --git a/src/Reflection/Mixin/MixinMethodsClassReflectionExtension.php b/src/Reflection/Mixin/MixinMethodsClassReflectionExtension.php index f5af02cc28..be29968120 100644 --- a/src/Reflection/Mixin/MixinMethodsClassReflectionExtension.php +++ b/src/Reflection/Mixin/MixinMethodsClassReflectionExtension.php @@ -6,20 +6,23 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\MethodsClassReflectionExtension; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\TypeUtils; +use PHPStan\Type\VerbosityLevel; +use function array_intersect; +use function count; class MixinMethodsClassReflectionExtension implements MethodsClassReflectionExtension { - /** @var string[] */ - private array $mixinExcludeClasses; + /** @var array> */ + private array $inProcess = []; /** * @param string[] $mixinExcludeClasses */ - public function __construct(array $mixinExcludeClasses) + public function __construct(private array $mixinExcludeClasses) { - $this->mixinExcludeClasses = $mixinExcludeClasses; } public function hasMethod(ClassReflection $classReflection, string $methodName): bool @@ -31,7 +34,7 @@ public function getMethod(ClassReflection $classReflection, string $methodName): { $method = $this->findMethod($classReflection, $methodName); if ($method === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $method; @@ -45,16 +48,26 @@ private function findMethod(ClassReflection $classReflection, string $methodName continue; } + $typeDescription = $type->describe(VerbosityLevel::typeOnly()); + if (isset($this->inProcess[$typeDescription][$methodName])) { + continue; + } + + $this->inProcess[$typeDescription][$methodName] = true; + if (!$type->hasMethod($methodName)->yes()) { + unset($this->inProcess[$typeDescription][$methodName]); continue; } $method = $type->getMethod($methodName, new OutOfClassScope()); + + unset($this->inProcess[$typeDescription][$methodName]); + $static = $method->isStatic(); if ( !$static && $classReflection->hasNativeMethod('__callStatic') - && !$classReflection->hasNativeMethod('__call') ) { $static = true; } diff --git a/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php b/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php index 4ab4635111..7e60150737 100644 --- a/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php +++ b/src/Reflection/Mixin/MixinPropertiesClassReflectionExtension.php @@ -6,20 +6,23 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\PropertiesClassReflectionExtension; use PHPStan\Reflection\PropertyReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\TypeUtils; +use PHPStan\Type\VerbosityLevel; +use function array_intersect; +use function count; class MixinPropertiesClassReflectionExtension implements PropertiesClassReflectionExtension { - /** @var string[] */ - private array $mixinExcludeClasses; + /** @var array> */ + private array $inProcess = []; /** * @param string[] $mixinExcludeClasses */ - public function __construct(array $mixinExcludeClasses) + public function __construct(private array $mixinExcludeClasses) { - $this->mixinExcludeClasses = $mixinExcludeClasses; } public function hasProperty(ClassReflection $classReflection, string $propertyName): bool @@ -31,7 +34,7 @@ public function getProperty(ClassReflection $classReflection, string $propertyNa { $property = $this->findProperty($classReflection, $propertyName); if ($property === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $property; @@ -45,11 +48,22 @@ private function findProperty(ClassReflection $classReflection, string $property continue; } + $typeDescription = $type->describe(VerbosityLevel::typeOnly()); + if (isset($this->inProcess[$typeDescription][$propertyName])) { + continue; + } + + $this->inProcess[$typeDescription][$propertyName] = true; + if (!$type->hasProperty($propertyName)->yes()) { + unset($this->inProcess[$typeDescription][$propertyName]); continue; } - return $type->getProperty($propertyName, new OutOfClassScope()); + $property = $type->getProperty($propertyName, new OutOfClassScope()); + unset($this->inProcess[$typeDescription][$propertyName]); + + return $property; } foreach ($classReflection->getParents() as $parentClass) { diff --git a/src/Reflection/Native/NativeFunctionReflection.php b/src/Reflection/Native/NativeFunctionReflection.php index 901d61e443..2142675d94 100644 --- a/src/Reflection/Native/NativeFunctionReflection.php +++ b/src/Reflection/Native/NativeFunctionReflection.php @@ -2,39 +2,26 @@ namespace PHPStan\Reflection\Native; +use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\ParametersAcceptor; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use PHPStan\Type\VoidType; -class NativeFunctionReflection implements \PHPStan\Reflection\FunctionReflection +class NativeFunctionReflection implements FunctionReflection { - private string $name; - - /** @var \PHPStan\Reflection\ParametersAcceptor[] */ - private array $variants; - - private ?\PHPStan\Type\Type $throwType; - - private TrinaryLogic $hasSideEffects; - /** - * @param string $name - * @param \PHPStan\Reflection\ParametersAcceptor[] $variants - * @param \PHPStan\Type\Type|null $throwType - * @param \PHPStan\TrinaryLogic $hasSideEffects + * @param ParametersAcceptor[] $variants */ public function __construct( - string $name, - array $variants, - ?Type $throwType, - TrinaryLogic $hasSideEffects + private string $name, + private array $variants, + private ?Type $throwType, + private TrinaryLogic $hasSideEffects, + private bool $isDeprecated, ) { - $this->name = $name; - $this->variants = $variants; - $this->throwType = $throwType; - $this->hasSideEffects = $hasSideEffects; } public function getName(): string @@ -42,8 +29,13 @@ public function getName(): string return $this->name; } + public function getFileName(): ?string + { + return null; + } + /** - * @return \PHPStan\Reflection\ParametersAcceptor[] + * @return ParametersAcceptor[] */ public function getVariants(): array { @@ -62,7 +54,7 @@ public function getDeprecatedDescription(): ?string public function isDeprecated(): TrinaryLogic { - return TrinaryLogic::createNo(); + return TrinaryLogic::createFromBoolean($this->isDeprecated); } public function isInternal(): TrinaryLogic diff --git a/src/Reflection/Native/NativeMethodReflection.php b/src/Reflection/Native/NativeMethodReflection.php index 1410a99583..5748707bb1 100644 --- a/src/Reflection/Native/NativeMethodReflection.php +++ b/src/Reflection/Native/NativeMethodReflection.php @@ -6,56 +6,31 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\MethodPrototypeReflection; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\Reflection\Php\BuiltinMethodReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; +use PHPStan\Type\TypehintHelper; use PHPStan\Type\VoidType; +use ReflectionException; +use function strtolower; class NativeMethodReflection implements MethodReflection { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Reflection\ClassReflection $declaringClass; - - private BuiltinMethodReflection $reflection; - - /** @var \PHPStan\Reflection\ParametersAcceptorWithPhpDocs[] */ - private array $variants; - - private TrinaryLogic $hasSideEffects; - - private ?string $stubPhpDocString; - - private ?Type $throwType; - /** - * @param \PHPStan\Reflection\ReflectionProvider $reflectionProvider - * @param \PHPStan\Reflection\ClassReflection $declaringClass - * @param BuiltinMethodReflection $reflection - * @param \PHPStan\Reflection\ParametersAcceptorWithPhpDocs[] $variants - * @param TrinaryLogic $hasSideEffects - * @param string|null $stubPhpDocString - * @param Type|null $throwType + * @param ParametersAcceptorWithPhpDocs[] $variants */ public function __construct( - ReflectionProvider $reflectionProvider, - ClassReflection $declaringClass, - BuiltinMethodReflection $reflection, - array $variants, - TrinaryLogic $hasSideEffects, - ?string $stubPhpDocString, - ?Type $throwType + private ReflectionProvider $reflectionProvider, + private ClassReflection $declaringClass, + private BuiltinMethodReflection $reflection, + private array $variants, + private TrinaryLogic $hasSideEffects, + private ?Type $throwType, ) { - $this->reflectionProvider = $reflectionProvider; - $this->declaringClass = $declaringClass; - $this->reflection = $reflection; - $this->variants = $variants; - $this->hasSideEffects = $hasSideEffects; - $this->stubPhpDocString = $stubPhpDocString; - $this->throwType = $throwType; } public function getDeclaringClass(): ClassReflection @@ -89,6 +64,11 @@ public function getPrototype(): ClassMemberReflection $prototypeMethod = $this->reflection->getPrototype(); $prototypeDeclaringClass = $this->reflectionProvider->getClass($prototypeMethod->getDeclaringClass()->getName()); + $tentativeReturnType = null; + if ($prototypeMethod->getTentativeReturnType() !== null) { + $tentativeReturnType = TypehintHelper::decideTypeFromReflection($prototypeMethod->getTentativeReturnType()); + } + return new MethodPrototypeReflection( $prototypeMethod->getName(), $prototypeDeclaringClass, @@ -97,9 +77,10 @@ public function getPrototype(): ClassMemberReflection $prototypeMethod->isPublic(), $prototypeMethod->isAbstract(), $prototypeMethod->isFinal(), - $prototypeDeclaringClass->getNativeMethod($prototypeMethod->getName())->getVariants() + $prototypeDeclaringClass->getNativeMethod($prototypeMethod->getName())->getVariants(), + $tentativeReturnType, ); - } catch (\ReflectionException $e) { + } catch (ReflectionException) { return $this; } } @@ -110,7 +91,7 @@ public function getName(): string } /** - * @return \PHPStan\Reflection\ParametersAcceptorWithPhpDocs[] + * @return ParametersAcceptorWithPhpDocs[] */ public function getVariants(): array { @@ -169,10 +150,6 @@ private function isVoid(): bool public function getDocComment(): ?string { - if ($this->stubPhpDocString !== null) { - return $this->stubPhpDocString; - } - return $this->reflection->getDocComment(); } diff --git a/src/Reflection/Native/NativeParameterReflection.php b/src/Reflection/Native/NativeParameterReflection.php index 3d15d91052..219f48069d 100644 --- a/src/Reflection/Native/NativeParameterReflection.php +++ b/src/Reflection/Native/NativeParameterReflection.php @@ -9,33 +9,15 @@ class NativeParameterReflection implements ParameterReflection { - private string $name; - - private bool $optional; - - private \PHPStan\Type\Type $type; - - private \PHPStan\Reflection\PassedByReference $passedByReference; - - private bool $variadic; - - private ?\PHPStan\Type\Type $defaultValue; - public function __construct( - string $name, - bool $optional, - Type $type, - PassedByReference $passedByReference, - bool $variadic, - ?Type $defaultValue + private string $name, + private bool $optional, + private Type $type, + private PassedByReference $passedByReference, + private bool $variadic, + private ?Type $defaultValue, ) { - $this->name = $name; - $this->optional = $optional; - $this->type = $type; - $this->passedByReference = $passedByReference; - $this->variadic = $variadic; - $this->defaultValue = $defaultValue; } public function getName(): string @@ -70,7 +52,6 @@ public function getDefaultValue(): ?Type /** * @param mixed[] $properties - * @return self */ public static function __set_state(array $properties): self { @@ -80,7 +61,7 @@ public static function __set_state(array $properties): self $properties['type'], $properties['passedByReference'], $properties['variadic'], - $properties['defaultValue'] + $properties['defaultValue'], ); } diff --git a/src/Reflection/Native/NativeParameterWithPhpDocsReflection.php b/src/Reflection/Native/NativeParameterWithPhpDocsReflection.php index f9f092fb4b..b5c01f9b90 100644 --- a/src/Reflection/Native/NativeParameterWithPhpDocsReflection.php +++ b/src/Reflection/Native/NativeParameterWithPhpDocsReflection.php @@ -9,41 +9,17 @@ class NativeParameterWithPhpDocsReflection implements ParameterReflectionWithPhpDocs { - private string $name; - - private bool $optional; - - private \PHPStan\Type\Type $type; - - private \PHPStan\Type\Type $phpDocType; - - private \PHPStan\Type\Type $nativeType; - - private \PHPStan\Reflection\PassedByReference $passedByReference; - - private bool $variadic; - - private ?\PHPStan\Type\Type $defaultValue; - public function __construct( - string $name, - bool $optional, - Type $type, - Type $phpDocType, - Type $nativeType, - PassedByReference $passedByReference, - bool $variadic, - ?Type $defaultValue + private string $name, + private bool $optional, + private Type $type, + private Type $phpDocType, + private Type $nativeType, + private PassedByReference $passedByReference, + private bool $variadic, + private ?Type $defaultValue, ) { - $this->name = $name; - $this->optional = $optional; - $this->type = $type; - $this->phpDocType = $phpDocType; - $this->nativeType = $nativeType; - $this->passedByReference = $passedByReference; - $this->variadic = $variadic; - $this->defaultValue = $defaultValue; } public function getName(): string @@ -88,7 +64,6 @@ public function getDefaultValue(): ?Type /** * @param mixed[] $properties - * @return self */ public static function __set_state(array $properties): self { @@ -100,7 +75,7 @@ public static function __set_state(array $properties): self $properties['nativeType'], $properties['passedByReference'], $properties['variadic'], - $properties['defaultValue'] + $properties['defaultValue'], ); } diff --git a/src/Reflection/ParametersAcceptor.php b/src/Reflection/ParametersAcceptor.php index 0902f90e58..f4c2d4f1c0 100644 --- a/src/Reflection/ParametersAcceptor.php +++ b/src/Reflection/ParametersAcceptor.php @@ -20,7 +20,7 @@ public function getTemplateTypeMap(): TemplateTypeMap; public function getResolvedTemplateTypeMap(): TemplateTypeMap; /** - * @return array + * @return array */ public function getParameters(): array; diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index a809100402..899a8a87d1 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -2,12 +2,27 @@ namespace PHPStan\Reflection; +use PhpParser\Node; +use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\Reflection\Native\NativeParameterReflection; +use PHPStan\Reflection\Php\DummyParameter; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\CallableType; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\MixedType; +use PHPStan\Type\NullType; +use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\UnionType; +use function array_slice; +use function count; +use function sprintf; +use const ARRAY_FILTER_USE_BOTH; +use const ARRAY_FILTER_USE_KEY; /** @api */ class ParametersAcceptorSelector @@ -19,30 +34,133 @@ class ParametersAcceptorSelector * @return T */ public static function selectSingle( - array $parametersAcceptors + array $parametersAcceptors, ): ParametersAcceptor { - if (count($parametersAcceptors) !== 1) { - throw new \PHPStan\ShouldNotHappenException(); + $count = count($parametersAcceptors); + if ($count === 0) { + throw new ShouldNotHappenException( + 'getVariants() must return at least one variant.', + ); + } + if ($count !== 1) { + throw new ShouldNotHappenException('Multiple variants - use selectFromArgs() instead.'); } return $parametersAcceptors[0]; } /** - * @param Scope $scope - * @param \PhpParser\Node\Arg[] $args + * @param Node\Arg[] $args * @param ParametersAcceptor[] $parametersAcceptors - * @return ParametersAcceptor */ public static function selectFromArgs( Scope $scope, array $args, - array $parametersAcceptors + array $parametersAcceptors, ): ParametersAcceptor { $types = []; $unpack = false; + if ( + count($args) > 0 + && count($parametersAcceptors) > 0 + ) { + $functionName = null; + $argParent = $args[0]->getAttribute('parent'); + if ($argParent instanceof FuncCall && $argParent->name instanceof Name) { + $functionName = $argParent->name->toLowerString(); + } + if ( + $functionName === 'array_map' + && isset($args[1]) + ) { + $acceptor = $parametersAcceptors[0]; + $parameters = $acceptor->getParameters(); + if (!isset($args[2])) { + $callbackParameters = [ + new DummyParameter('item', $scope->getType($args[1]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null), + ]; + } else { + $callbackParameters = []; + foreach ($args as $i => $arg) { + if ($i === 0) { + continue; + } + + $callbackParameters[] = new DummyParameter('item', $scope->getType($arg->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null); + } + } + $parameters[0] = new NativeParameterReflection( + $parameters[0]->getName(), + $parameters[0]->isOptional(), + new UnionType([ + new CallableType($callbackParameters, new MixedType(), false), + new NullType(), + ]), + $parameters[0]->passedByReference(), + $parameters[0]->isVariadic(), + $parameters[0]->getDefaultValue(), + ); + $parametersAcceptors = [ + new FunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + $parameters, + $acceptor->isVariadic(), + $acceptor->getReturnType(), + ), + ]; + } + + if ( + $functionName === 'array_filter' + && isset($args[0]) + ) { + if (isset($args[2])) { + $mode = $scope->getType($args[2]->value); + if ($mode instanceof ConstantIntegerType) { + if ($mode->getValue() === ARRAY_FILTER_USE_KEY) { + $arrayFilterParameters = [ + new DummyParameter('key', $scope->getType($args[0]->value)->getIterableKeyType(), false, PassedByReference::createNo(), false, null), + ]; + } elseif ($mode->getValue() === ARRAY_FILTER_USE_BOTH) { + $arrayFilterParameters = [ + new DummyParameter('item', $scope->getType($args[0]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null), + new DummyParameter('key', $scope->getType($args[0]->value)->getIterableKeyType(), false, PassedByReference::createNo(), false, null), + ]; + } + } + } + + $acceptor = $parametersAcceptors[0]; + $parameters = $acceptor->getParameters(); + $parameters[1] = new NativeParameterReflection( + $parameters[1]->getName(), + $parameters[1]->isOptional(), + new CallableType( + $arrayFilterParameters ?? [ + new DummyParameter('item', $scope->getType($args[0]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null), + ], + new MixedType(), + false, + ), + $parameters[1]->passedByReference(), + $parameters[1]->isVariadic(), + $parameters[1]->getDefaultValue(), + ); + $parametersAcceptors = [ + new FunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + $parameters, + $acceptor->isVariadic(), + $acceptor->getReturnType(), + ), + ]; + } + } + foreach ($args as $arg) { $type = $scope->getType($arg->value); if ($arg->unpack) { @@ -57,15 +175,13 @@ public static function selectFromArgs( } /** - * @param \PHPStan\Type\Type[] $types + * @param Type[] $types * @param ParametersAcceptor[] $parametersAcceptors - * @param bool $unpack - * @return ParametersAcceptor */ public static function selectFromTypes( array $types, array $parametersAcceptors, - bool $unpack + bool $unpack, ): ParametersAcceptor { if (count($parametersAcceptors) === 1) { @@ -73,8 +189,8 @@ public static function selectFromTypes( } if (count($parametersAcceptors) === 0) { - throw new \PHPStan\ShouldNotHappenException( - 'getVariants() must return at least one variant.' + throw new ShouldNotHappenException( + 'getVariants() must return at least one variant.', ); } @@ -169,13 +285,12 @@ public static function selectFromTypes( /** * @param ParametersAcceptor[] $acceptors - * @return ParametersAcceptor */ public static function combineAcceptors(array $acceptors): ParametersAcceptor { if (count($acceptors) === 0) { - throw new \PHPStan\ShouldNotHappenException( - 'getVariants() must return at least one variant.' + throw new ShouldNotHappenException( + 'getVariants() must return at least one variant.', ); } if (count($acceptors) === 1) { @@ -220,7 +335,7 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptor $parameter->getType(), $parameter->passedByReference(), $parameter->isVariadic(), - $parameter->getDefaultValue() + $parameter->getDefaultValue(), ); continue; } @@ -240,7 +355,7 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptor TypeCombinator::union($parameters[$i]->getType(), $parameter->getType()), $parameters[$i]->passedByReference()->combine($parameter->passedByReference()), $isVariadic, - $defaultValue + $defaultValue, ); if ($isVariadic) { @@ -255,7 +370,7 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptor null, $parameters, $isVariadic, - $returnType + $returnType, ); } diff --git a/src/Reflection/ParametersAcceptorWithPhpDocs.php b/src/Reflection/ParametersAcceptorWithPhpDocs.php index de4c7e675f..11200bc1f5 100644 --- a/src/Reflection/ParametersAcceptorWithPhpDocs.php +++ b/src/Reflection/ParametersAcceptorWithPhpDocs.php @@ -9,7 +9,7 @@ interface ParametersAcceptorWithPhpDocs extends ParametersAcceptor { /** - * @return array + * @return array */ public function getParameters(): array; diff --git a/src/Reflection/PassedByReference.php b/src/Reflection/PassedByReference.php index 663dcbe3fb..d4741bc1b6 100644 --- a/src/Reflection/PassedByReference.php +++ b/src/Reflection/PassedByReference.php @@ -2,6 +2,8 @@ namespace PHPStan\Reflection; +use function array_key_exists; + /** @api */ class PassedByReference { @@ -13,11 +15,8 @@ class PassedByReference /** @var self[] */ private static array $registry = []; - private int $value; - - private function __construct(int $value) + private function __construct(private int $value) { - $this->value = $value; } private static function create(int $value): self @@ -77,7 +76,6 @@ public function combine(self $other): self /** * @param mixed[] $properties - * @return self */ public static function __set_state(array $properties): self { diff --git a/src/Reflection/Php/BuiltinMethodReflection.php b/src/Reflection/Php/BuiltinMethodReflection.php index efc2f4e0b4..50a5956c7a 100644 --- a/src/Reflection/Php/BuiltinMethodReflection.php +++ b/src/Reflection/Php/BuiltinMethodReflection.php @@ -3,30 +3,25 @@ namespace PHPStan\Reflection\Php; use PHPStan\TrinaryLogic; +use ReflectionClass; +use ReflectionMethod; +use ReflectionParameter; +use ReflectionType; interface BuiltinMethodReflection { public function getName(): string; - public function getReflection(): ?\ReflectionMethod; + public function getReflection(): ?ReflectionMethod; - /** - * @return string|false - */ - public function getFileName(); + public function getFileName(): ?string; - public function getDeclaringClass(): \ReflectionClass; + public function getDeclaringClass(): ReflectionClass; - /** - * @return int|false - */ - public function getStartLine(); + public function getStartLine(): ?int; - /** - * @return int|false - */ - public function getEndLine(); + public function getEndLine(): ?int; public function getDocComment(): ?string; @@ -42,10 +37,12 @@ public function isDeprecated(): TrinaryLogic; public function isVariadic(): bool; - public function getReturnType(): ?\ReflectionType; + public function getReturnType(): ?ReflectionType; + + public function getTentativeReturnType(): ?ReflectionType; /** - * @return \ReflectionParameter[] + * @return ReflectionParameter[] */ public function getParameters(): array; diff --git a/src/Reflection/Php/ClosureCallMethodReflection.php b/src/Reflection/Php/ClosureCallMethodReflection.php index 4c598bcaae..ac25944bdd 100644 --- a/src/Reflection/Php/ClosureCallMethodReflection.php +++ b/src/Reflection/Php/ClosureCallMethodReflection.php @@ -7,26 +7,22 @@ use PHPStan\Reflection\FunctionVariant; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\Native\NativeParameterReflection; +use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\PassedByReference; use PHPStan\TrinaryLogic; use PHPStan\Type\ClosureType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; +use function array_unshift; final class ClosureCallMethodReflection implements MethodReflection { - private MethodReflection $nativeMethodReflection; - - private ClosureType $closureType; - public function __construct( - MethodReflection $nativeMethodReflection, - ClosureType $closureType + private MethodReflection $nativeMethodReflection, + private ClosureType $closureType, ) { - $this->nativeMethodReflection = $nativeMethodReflection; - $this->closureType = $closureType; } public function getDeclaringClass(): ClassReflection @@ -65,7 +61,7 @@ public function getPrototype(): ClassMemberReflection } /** - * @return \PHPStan\Reflection\ParametersAcceptor[] + * @return ParametersAcceptor[] */ public function getVariants(): array { @@ -76,7 +72,7 @@ public function getVariants(): array new ObjectWithoutClassType(), PassedByReference::createNo(), false, - null + null, ); array_unshift($parameters, $newThis); @@ -87,7 +83,7 @@ public function getVariants(): array $this->closureType->getResolvedTemplateTypeMap(), $parameters, $this->closureType->isVariadic(), - $this->closureType->getReturnType() + $this->closureType->getReturnType(), ), ]; } diff --git a/src/Reflection/Php/ClosureCallUnresolvedMethodPrototypeReflection.php b/src/Reflection/Php/ClosureCallUnresolvedMethodPrototypeReflection.php index 3ec871b59e..a67891dd95 100644 --- a/src/Reflection/Php/ClosureCallUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Php/ClosureCallUnresolvedMethodPrototypeReflection.php @@ -10,14 +10,8 @@ class ClosureCallUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection { - private UnresolvedMethodPrototypeReflection $prototype; - - private ClosureType $closure; - - public function __construct(UnresolvedMethodPrototypeReflection $prototype, ClosureType $closure) + public function __construct(private UnresolvedMethodPrototypeReflection $prototype, private ClosureType $closure) { - $this->prototype = $prototype; - $this->closure = $closure; } public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototypeReflection diff --git a/src/Reflection/Php/DummyParameter.php b/src/Reflection/Php/DummyParameter.php index 1420d4814f..5d73990a42 100644 --- a/src/Reflection/Php/DummyParameter.php +++ b/src/Reflection/Php/DummyParameter.php @@ -9,27 +9,11 @@ class DummyParameter implements ParameterReflection { - private string $name; + private PassedByReference $passedByReference; - private \PHPStan\Type\Type $type; - - private bool $optional; - - private \PHPStan\Reflection\PassedByReference $passedByReference; - - private bool $variadic; - - /** @var ?\PHPStan\Type\Type */ - private ?\PHPStan\Type\Type $defaultValue; - - public function __construct(string $name, Type $type, bool $optional, ?PassedByReference $passedByReference, bool $variadic, ?Type $defaultValue) + public function __construct(private string $name, private Type $type, private bool $optional, ?PassedByReference $passedByReference, private bool $variadic, private ?Type $defaultValue) { - $this->name = $name; - $this->type = $type; - $this->optional = $optional; $this->passedByReference = $passedByReference ?? PassedByReference::createNo(); - $this->variadic = $variadic; - $this->defaultValue = $defaultValue; } public function getName(): string diff --git a/src/Reflection/Php/EnumCasesMethodReflection.php b/src/Reflection/Php/EnumCasesMethodReflection.php new file mode 100644 index 0000000000..d5f86b45c8 --- /dev/null +++ b/src/Reflection/Php/EnumCasesMethodReflection.php @@ -0,0 +1,108 @@ +declaringClass; + } + + public function isStatic(): bool + { + return true; + } + + public function isPrivate(): bool + { + return false; + } + + public function isPublic(): bool + { + return true; + } + + public function getDocComment(): ?string + { + return null; + } + + public function getName(): string + { + return 'cases'; + } + + public function getPrototype(): ClassMemberReflection + { + $unitEnum = $this->declaringClass->getAncestorWithClassName('UnitEnum'); + if ($unitEnum === null) { + throw new ShouldNotHappenException(); + } + + return $unitEnum->getNativeMethod('cases'); + } + + /** + * @return ParametersAcceptor[] + */ + public function getVariants(): array + { + return [ + new FunctionVariant( + TemplateTypeMap::createEmpty(), + TemplateTypeMap::createEmpty(), + [], + false, + $this->returnType, + ), + ]; + } + + public function isDeprecated(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getDeprecatedDescription(): ?string + { + return null; + } + + public function isFinal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isInternal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getThrowType(): ?Type + { + return null; + } + + public function hasSideEffects(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + +} diff --git a/src/Reflection/Php/EnumPropertyReflection.php b/src/Reflection/Php/EnumPropertyReflection.php new file mode 100644 index 0000000000..e3f7927dec --- /dev/null +++ b/src/Reflection/Php/EnumPropertyReflection.php @@ -0,0 +1,82 @@ +declaringClass; + } + + 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 $this->type; + } + + public function canChangeTypeAfterAssignment(): bool + { + return true; + } + + 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/Reflection/Php/FakeBuiltinMethodReflection.php b/src/Reflection/Php/FakeBuiltinMethodReflection.php index a1737af26c..c190c3f6e3 100644 --- a/src/Reflection/Php/FakeBuiltinMethodReflection.php +++ b/src/Reflection/Php/FakeBuiltinMethodReflection.php @@ -3,21 +3,20 @@ namespace PHPStan\Reflection\Php; use PHPStan\TrinaryLogic; +use ReflectionClass; +use ReflectionException; +use ReflectionMethod; +use ReflectionParameter; +use ReflectionType; class FakeBuiltinMethodReflection implements BuiltinMethodReflection { - private string $methodName; - - private \ReflectionClass $declaringClass; - public function __construct( - string $methodName, - \ReflectionClass $declaringClass + private string $methodName, + private ReflectionClass $declaringClass, ) { - $this->methodName = $methodName; - $this->declaringClass = $declaringClass; } public function getName(): string @@ -25,38 +24,29 @@ public function getName(): string return $this->methodName; } - public function getReflection(): ?\ReflectionMethod + public function getReflection(): ?ReflectionMethod { return null; } - /** - * @return string|false - */ - public function getFileName() + public function getFileName(): ?string { - return false; + return null; } - public function getDeclaringClass(): \ReflectionClass + public function getDeclaringClass(): ReflectionClass { return $this->declaringClass; } - /** - * @return int|false - */ - public function getStartLine() + public function getStartLine(): ?int { - return false; + return null; } - /** - * @return int|false - */ - public function getEndLine() + public function getEndLine(): ?int { - return false; + return null; } public function getDocComment(): ?string @@ -81,7 +71,7 @@ public function isPublic(): bool public function getPrototype(): BuiltinMethodReflection { - throw new \ReflectionException(); + throw new ReflectionException(); } public function isDeprecated(): TrinaryLogic @@ -109,13 +99,18 @@ public function isAbstract(): bool return false; } - public function getReturnType(): ?\ReflectionType + public function getReturnType(): ?ReflectionType + { + return null; + } + + public function getTentativeReturnType(): ?ReflectionType { return null; } /** - * @return \ReflectionParameter[] + * @return ReflectionParameter[] */ public function getParameters(): array { diff --git a/src/Reflection/Php/NativeBuiltinMethodReflection.php b/src/Reflection/Php/NativeBuiltinMethodReflection.php index 8a59bb0237..b9286a490b 100644 --- a/src/Reflection/Php/NativeBuiltinMethodReflection.php +++ b/src/Reflection/Php/NativeBuiltinMethodReflection.php @@ -3,15 +3,17 @@ namespace PHPStan\Reflection\Php; use PHPStan\TrinaryLogic; +use ReflectionClass; +use ReflectionMethod; +use ReflectionParameter; +use ReflectionType; +use function method_exists; class NativeBuiltinMethodReflection implements BuiltinMethodReflection { - private \ReflectionMethod $reflection; - - public function __construct(\ReflectionMethod $reflection) + public function __construct(private ReflectionMethod $reflection) { - $this->reflection = $reflection; } public function getName(): string @@ -19,38 +21,44 @@ public function getName(): string return $this->reflection->getName(); } - public function getReflection(): ?\ReflectionMethod + public function getReflection(): ?ReflectionMethod { return $this->reflection; } - /** - * @return string|false - */ - public function getFileName() + public function getFileName(): ?string { - return $this->reflection->getFileName(); + $fileName = $this->reflection->getFileName(); + if ($fileName === false) { + return null; + } + + return $fileName; } - public function getDeclaringClass(): \ReflectionClass + public function getDeclaringClass(): ReflectionClass { return $this->reflection->getDeclaringClass(); } - /** - * @return int|false - */ - public function getStartLine() + public function getStartLine(): ?int { - return $this->reflection->getStartLine(); + $line = $this->reflection->getStartLine(); + if ($line === false) { + return null; + } + + return $line; } - /** - * @return int|false - */ - public function getEndLine() + public function getEndLine(): ?int { - return $this->reflection->getEndLine(); + $line = $this->reflection->getEndLine(); + if ($line === false) { + return null; + } + + return $line; } public function getDocComment(): ?string @@ -113,13 +121,22 @@ public function isVariadic(): bool return $this->reflection->isVariadic(); } - public function getReturnType(): ?\ReflectionType + public function getReturnType(): ?ReflectionType { return $this->reflection->getReturnType(); } + public function getTentativeReturnType(): ?ReflectionType + { + if (method_exists($this->reflection, 'getTentativeReturnType')) { + return $this->reflection->getTentativeReturnType(); + } + + return null; + } + /** - * @return \ReflectionParameter[] + * @return ReflectionParameter[] */ public function getParameters(): array { diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 0bd6ffd796..c1fb32a16e 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -30,60 +30,55 @@ use PHPStan\Reflection\SignatureMap\FunctionSignature; use PHPStan\Reflection\SignatureMap\ParameterSignature; use PHPStan\Reflection\SignatureMap\SignatureMapProvider; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantArrayTypeBuilder; +use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\ErrorType; use PHPStan\Type\FileTypeMapper; +use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypehintHelper; -use PHPStan\Type\TypeUtils; +use ReflectionClass; +use ReflectionParameter; +use function array_key_exists; +use function array_keys; +use function array_map; +use function array_shift; +use function array_slice; +use function class_exists; +use function count; +use function explode; +use function implode; +use function in_array; +use function is_array; +use function method_exists; +use function reset; +use function sprintf; +use function strtolower; class PhpClassReflectionExtension implements PropertiesClassReflectionExtension, MethodsClassReflectionExtension { - private ScopeFactory $scopeFactory; - - private NodeScopeResolver $nodeScopeResolver; - - private \PHPStan\Reflection\Php\PhpMethodReflectionFactory $methodReflectionFactory; - - private \PHPStan\PhpDoc\PhpDocInheritanceResolver $phpDocInheritanceResolver; - - private \PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension $annotationsMethodsClassReflectionExtension; - - private \PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension $annotationsPropertiesClassReflectionExtension; - - private \PHPStan\Reflection\SignatureMap\SignatureMapProvider $signatureMapProvider; - - private \PHPStan\Parser\Parser $parser; - - private \PHPStan\PhpDoc\StubPhpDocProvider $stubPhpDocProvider; - - private bool $inferPrivatePropertyTypeFromConstructor; - - private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider; - - private FileTypeMapper $fileTypeMapper; - - /** @var string[] */ - private array $universalObjectCratesClasses; - - /** @var \PHPStan\Reflection\PropertyReflection[][] */ + /** @var PropertyReflection[][] */ private array $propertiesIncludingAnnotations = []; - /** @var \PHPStan\Reflection\Php\PhpPropertyReflection[][] */ + /** @var PhpPropertyReflection[][] */ private array $nativeProperties = []; - /** @var \PHPStan\Reflection\MethodReflection[][] */ + /** @var MethodReflection[][] */ private array $methodsIncludingAnnotations = []; - /** @var \PHPStan\Reflection\MethodReflection[][] */ + /** @var MethodReflection[][] */ private array $nativeMethods = []; /** @var array> */ @@ -93,49 +88,24 @@ class PhpClassReflectionExtension private array $inferClassConstructorPropertyTypesInProcess = []; /** - * @param \PHPStan\Analyser\ScopeFactory $scopeFactory - * @param \PHPStan\Analyser\NodeScopeResolver $nodeScopeResolver - * @param \PHPStan\Reflection\Php\PhpMethodReflectionFactory $methodReflectionFactory - * @param \PHPStan\PhpDoc\PhpDocInheritanceResolver $phpDocInheritanceResolver - * @param \PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension $annotationsMethodsClassReflectionExtension - * @param \PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension $annotationsPropertiesClassReflectionExtension - * @param \PHPStan\Reflection\SignatureMap\SignatureMapProvider $signatureMapProvider - * @param \PHPStan\Parser\Parser $parser - * @param \PHPStan\PhpDoc\StubPhpDocProvider $stubPhpDocProvider - * @param ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider - * @param FileTypeMapper $fileTypeMapper - * @param bool $inferPrivatePropertyTypeFromConstructor * @param string[] $universalObjectCratesClasses */ public function __construct( - ScopeFactory $scopeFactory, - NodeScopeResolver $nodeScopeResolver, - PhpMethodReflectionFactory $methodReflectionFactory, - PhpDocInheritanceResolver $phpDocInheritanceResolver, - AnnotationsMethodsClassReflectionExtension $annotationsMethodsClassReflectionExtension, - AnnotationsPropertiesClassReflectionExtension $annotationsPropertiesClassReflectionExtension, - SignatureMapProvider $signatureMapProvider, - Parser $parser, - StubPhpDocProvider $stubPhpDocProvider, - ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, - FileTypeMapper $fileTypeMapper, - bool $inferPrivatePropertyTypeFromConstructor, - array $universalObjectCratesClasses + private ScopeFactory $scopeFactory, + private NodeScopeResolver $nodeScopeResolver, + private PhpMethodReflectionFactory $methodReflectionFactory, + private PhpDocInheritanceResolver $phpDocInheritanceResolver, + private AnnotationsMethodsClassReflectionExtension $annotationsMethodsClassReflectionExtension, + private AnnotationsPropertiesClassReflectionExtension $annotationsPropertiesClassReflectionExtension, + private SignatureMapProvider $signatureMapProvider, + private Parser $parser, + private StubPhpDocProvider $stubPhpDocProvider, + private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, + private FileTypeMapper $fileTypeMapper, + private bool $inferPrivatePropertyTypeFromConstructor, + private array $universalObjectCratesClasses, ) { - $this->scopeFactory = $scopeFactory; - $this->nodeScopeResolver = $nodeScopeResolver; - $this->methodReflectionFactory = $methodReflectionFactory; - $this->phpDocInheritanceResolver = $phpDocInheritanceResolver; - $this->annotationsMethodsClassReflectionExtension = $annotationsMethodsClassReflectionExtension; - $this->annotationsPropertiesClassReflectionExtension = $annotationsPropertiesClassReflectionExtension; - $this->signatureMapProvider = $signatureMapProvider; - $this->parser = $parser; - $this->stubPhpDocProvider = $stubPhpDocProvider; - $this->reflectionProviderProvider = $reflectionProviderProvider; - $this->fileTypeMapper = $fileTypeMapper; - $this->inferPrivatePropertyTypeFromConstructor = $inferPrivatePropertyTypeFromConstructor; - $this->universalObjectCratesClasses = $universalObjectCratesClasses; } public function hasProperty(ClassReflection $classReflection, string $propertyName): bool @@ -155,7 +125,7 @@ public function getProperty(ClassReflection $classReflection, string $propertyNa public function getNativeProperty(ClassReflection $classReflection, string $propertyName): PhpPropertyReflection { if (!isset($this->nativeProperties[$classReflection->getCacheKey()][$propertyName])) { - /** @var \PHPStan\Reflection\Php\PhpPropertyReflection $property */ + /** @var PhpPropertyReflection $property */ $property = $this->createProperty($classReflection, $propertyName, false); $this->nativeProperties[$classReflection->getCacheKey()][$propertyName] = $property; } @@ -166,7 +136,7 @@ public function getNativeProperty(ClassReflection $classReflection, string $prop private function createProperty( ClassReflection $classReflection, string $propertyName, - bool $includingAnnotations + bool $includingAnnotations, ): PropertyReflection { $propertyReflection = $classReflection->getNativeReflection()->getProperty($propertyName); @@ -174,22 +144,51 @@ private function createProperty( $declaringClassName = $propertyReflection->getDeclaringClass()->getName(); $declaringClassReflection = $classReflection->getAncestorWithClassName($declaringClassName); if ($declaringClassReflection === null) { - throw new \PHPStan\ShouldNotHappenException(sprintf( + throw new ShouldNotHappenException(sprintf( 'Internal error: Expected to find an ancestor with class name %s on %s, but none was found.', $declaringClassName, - $classReflection->getName() + $classReflection->getName(), )); } + if ($declaringClassReflection->isEnum()) { + if ( + $propertyName === 'name' + || ($declaringClassReflection->isBackedEnum() && $propertyName === 'value') + ) { + $types = []; + foreach (array_keys($classReflection->getEnumCases()) as $name) { + if ($propertyName === 'name') { + $types[] = new ConstantStringType($name); + continue; + } + + $case = $classReflection->getEnumCase($name); + $value = $case->getBackingValueType(); + if ($value === null) { + throw new ShouldNotHappenException(); + } + + $types[] = $value; + } + + return new PhpPropertyReflection($declaringClassReflection, null, null, TypeCombinator::union(...$types), $classReflection->getNativeReflection()->getProperty($propertyName), null, false, false); + } + } + $deprecatedDescription = null; $isDeprecated = false; $isInternal = false; - if ($includingAnnotations && $this->annotationsPropertiesClassReflectionExtension->hasProperty($classReflection, $propertyName)) { + if ( + $includingAnnotations + && !$declaringClassReflection->isEnum() + && $this->annotationsPropertiesClassReflectionExtension->hasProperty($classReflection, $propertyName) + ) { $hierarchyDistances = $classReflection->getClassHierarchyDistances(); $annotationProperty = $this->annotationsPropertiesClassReflectionExtension->getProperty($classReflection, $propertyName); if (!isset($hierarchyDistances[$annotationProperty->getDeclaringClass()->getName()])) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $distanceDeclaringClass = $propertyReflection->getDeclaringClass()->getName(); @@ -198,7 +197,7 @@ private function createProperty( $distanceDeclaringClass = $propertyTrait; } if (!isset($hierarchyDistances[$distanceDeclaringClass])) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ($hierarchyDistances[$annotationProperty->getDeclaringClass()->getName()] < $hierarchyDistances[$distanceDeclaringClass]) { @@ -212,43 +211,34 @@ private function createProperty( $declaringTraitName = null; $phpDocType = null; - $resolvedPhpDoc = $this->stubPhpDocProvider->findPropertyPhpDoc( - $declaringClassName, - $propertyReflection->getName() - ); - $stubPhpDocString = null; - if ($resolvedPhpDoc === null) { - if ($declaringClassReflection->getFileName() !== false) { - $declaringTraitName = $this->findPropertyTrait($propertyReflection); - $constructorName = null; - if (method_exists($propertyReflection, 'isPromoted') && $propertyReflection->isPromoted()) { - if ($declaringClassReflection->hasConstructor()) { - $constructorName = $declaringClassReflection->getConstructor()->getName(); - } + $resolvedPhpDoc = null; + if ($declaringClassReflection->getFileName() !== null) { + $declaringTraitName = $this->findPropertyTrait($propertyReflection); + $constructorName = null; + if (method_exists($propertyReflection, 'isPromoted') && $propertyReflection->isPromoted()) { + if ($declaringClassReflection->hasConstructor()) { + $constructorName = $declaringClassReflection->getConstructor()->getName(); } + } - if ($constructorName === null) { - $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForProperty( - $docComment, - $declaringClassReflection, - $declaringClassReflection->getFileName(), - $declaringTraitName, - $propertyName - ); - } elseif ($docComment !== null) { - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $declaringClassReflection->getFileName(), - $declaringClassName, - $declaringTraitName, - $constructorName, - $docComment - ); - } - $phpDocBlockClassReflection = $declaringClassReflection; + if ($constructorName === null) { + $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForProperty( + $docComment, + $declaringClassReflection, + $declaringClassReflection->getFileName(), + $declaringTraitName, + $propertyName, + ); + } elseif ($docComment !== null) { + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $declaringClassReflection->getFileName(), + $declaringClassName, + $declaringTraitName, + $constructorName, + $docComment, + ); } - } else { $phpDocBlockClassReflection = $declaringClassReflection; - $stubPhpDocString = $resolvedPhpDoc->getPhpDocString(); } if ($resolvedPhpDoc !== null) { @@ -261,14 +251,12 @@ private function createProperty( } if ($phpDocType === null) { - if (isset($constructorName) && $declaringClassReflection->getFileName() !== false) { + if (isset($constructorName) && $declaringClassReflection->getFileName() !== null) { $constructorDocComment = $declaringClassReflection->getConstructor()->getDocComment(); $nativeClassReflection = $declaringClassReflection->getNativeReflection(); $positionalParameterNames = []; if ($nativeClassReflection->getConstructor() !== null) { - $positionalParameterNames = array_map(static function (\ReflectionParameter $parameter): string { - return $parameter->getName(); - }, $nativeClassReflection->getConstructor()->getParameters()); + $positionalParameterNames = array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $nativeClassReflection->getConstructor()->getParameters()); } $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForMethod( $constructorDocComment, @@ -276,7 +264,7 @@ private function createProperty( $declaringClassReflection, $declaringTraitName, $constructorName, - $positionalParameterNames + $positionalParameterNames, ); $paramTags = $resolvedPhpDoc->getParamTags(); if (isset($paramTags[$propertyReflection->getName()])) { @@ -287,11 +275,11 @@ private function createProperty( if ($resolvedPhpDoc !== null) { if (!isset($phpDocBlockClassReflection)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $phpDocType = $phpDocType !== null ? TemplateTypeHelper::resolveTemplateTypes( $phpDocType, - $phpDocBlockClassReflection->getActiveTemplateTypeMap() + $phpDocBlockClassReflection->getActiveTemplateTypeMap(), ) : null; $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null; $isDeprecated = $resolvedPhpDoc->isDeprecated(); @@ -301,7 +289,7 @@ private function createProperty( if ( $phpDocType === null && $this->inferPrivatePropertyTypeFromConstructor - && $declaringClassReflection->getFileName() !== false + && $declaringClassReflection->getFileName() !== null && $propertyReflection->isPrivate() && (!method_exists($propertyReflection, 'hasType') || !$propertyReflection->hasType()) && $declaringClassReflection->hasConstructor() @@ -309,7 +297,7 @@ private function createProperty( ) { $phpDocType = $this->inferPrivatePropertyType( $propertyReflection->getName(), - $declaringClassReflection->getConstructor() + $declaringClassReflection->getConstructor(), ); } @@ -335,7 +323,6 @@ private function createProperty( $deprecatedDescription, $isDeprecated, $isInternal, - $stubPhpDocString ); } @@ -372,7 +359,7 @@ public function hasNativeMethod(ClassReflection $classReflection, string $method if ($methodName === '__get' && UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate( $this->reflectionProviderProvider->getReflectionProvider(), $this->universalObjectCratesClasses, - $classReflection + $classReflection, )) { return true; } @@ -388,7 +375,7 @@ public function getNativeMethod(ClassReflection $classReflection, string $method if ($classReflection->getNativeReflection()->hasMethod($methodName)) { $nativeMethodReflection = new NativeBuiltinMethodReflection( - $classReflection->getNativeReflection()->getMethod($methodName) + $classReflection->getNativeReflection()->getMethod($methodName), ); } else { if ( @@ -396,14 +383,14 @@ public function getNativeMethod(ClassReflection $classReflection, string $method || !UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate( $this->reflectionProviderProvider->getReflectionProvider(), $this->universalObjectCratesClasses, - $classReflection + $classReflection, )) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $nativeMethodReflection = new FakeBuiltinMethodReflection( $methodName, - $classReflection->getNativeReflection() + $classReflection->getNativeReflection(), ); } @@ -418,14 +405,14 @@ public function getNativeMethod(ClassReflection $classReflection, string $method private function createMethod( ClassReflection $classReflection, BuiltinMethodReflection $methodReflection, - bool $includingAnnotations + bool $includingAnnotations, ): MethodReflection { if ($includingAnnotations && $this->annotationsMethodsClassReflectionExtension->hasMethod($classReflection, $methodReflection->getName())) { $hierarchyDistances = $classReflection->getClassHierarchyDistances(); $annotationMethod = $this->annotationsMethodsClassReflectionExtension->getMethod($classReflection, $methodReflection->getName()); if (!isset($hierarchyDistances[$annotationMethod->getDeclaringClass()->getName()])) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $distanceDeclaringClass = $methodReflection->getDeclaringClass()->getName(); @@ -434,7 +421,7 @@ private function createMethod( $distanceDeclaringClass = $methodTrait; } if (!isset($hierarchyDistances[$distanceDeclaringClass])) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ($hierarchyDistances[$annotationMethod->getDeclaringClass()->getName()] < $hierarchyDistances[$distanceDeclaringClass]) { @@ -445,29 +432,41 @@ private function createMethod( $declaringClass = $classReflection->getAncestorWithClassName($declaringClassName); if ($declaringClass === null) { - throw new \PHPStan\ShouldNotHappenException(sprintf( + throw new ShouldNotHappenException(sprintf( 'Internal error: Expected to find an ancestor with class name %s on %s, but none was found.', $declaringClassName, - $classReflection->getName() + $classReflection->getName(), )); } + if ( + $declaringClass->isEnum() + && $declaringClass->getName() !== 'UnitEnum' + && strtolower($methodReflection->getName()) === 'cases' + ) { + $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); + foreach (array_keys($classReflection->getEnumCases()) as $name) { + $arrayBuilder->setOffsetValueType(null, new EnumCaseObjectType($classReflection->getName(), $name)); + } + + return new EnumCasesMethodReflection($declaringClass, $arrayBuilder->getArray()); + } + if ($this->signatureMapProvider->hasMethodSignature($declaringClassName, $methodReflection->getName())) { - $variantNumbers = []; - $i = 0; + $variantNumbers = [0]; + $i = 1; while ($this->signatureMapProvider->hasMethodSignature($declaringClassName, $methodReflection->getName(), $i)) { $variantNumbers[] = $i; $i++; } - $stubPhpDocString = null; $variants = []; $reflectionMethod = null; $throwType = null; if ($classReflection->getNativeReflection()->hasMethod($methodReflection->getName())) { $reflectionMethod = $classReflection->getNativeReflection()->getMethod($methodReflection->getName()); } elseif (class_exists($classReflection->getName(), false)) { - $reflectionClass = new \ReflectionClass($classReflection->getName()); + $reflectionClass = new ReflectionClass($classReflection->getName()); if ($reflectionClass->hasMethod($methodReflection->getName())) { $reflectionMethod = $reflectionClass->getMethod($methodReflection->getName()); } @@ -485,25 +484,22 @@ private function createMethod( $phpDocReturnType = null; $stubPhpDocPair = null; if (count($variantNumbers) === 1) { - $stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors($declaringClass, $methodReflection->getName(), array_map(static function (ParameterSignature $parameterSignature): string { - return $parameterSignature->getName(); - }, $methodSignature->getParameters())); + $stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors($declaringClass, $methodReflection->getName(), array_map(static fn (ParameterSignature $parameterSignature): string => $parameterSignature->getName(), $methodSignature->getParameters())); if ($stubPhpDocPair !== null) { [$stubPhpDoc, $stubDeclaringClass] = $stubPhpDocPair; - $stubPhpDocString = $stubPhpDoc->getPhpDocString(); $templateTypeMap = $stubDeclaringClass->getActiveTemplateTypeMap(); $returnTag = $stubPhpDoc->getReturnTag(); if ($returnTag !== null) { $stubPhpDocReturnType = TemplateTypeHelper::resolveTemplateTypes( $returnTag->getType(), - $templateTypeMap + $templateTypeMap, ); } foreach ($stubPhpDoc->getParamTags() as $name => $paramTag) { $stubPhpDocParameterTypes[$name] = TemplateTypeHelper::resolveTemplateTypes( $paramTag->getType(), - $templateTypeMap + $templateTypeMap, ); $stubPhpDocParameterVariadicity[$name] = $paramTag->isVariadic(); } @@ -522,7 +518,7 @@ private function createMethod( $declaringClassName, null, $reflectionMethod->getName(), - $reflectionMethod->getDocComment() + $reflectionMethod->getDocComment(), ); $throwsTag = $phpDocBlock->getThrowsTag(); if ($throwsTag !== null) { @@ -560,28 +556,22 @@ private function createMethod( $methodReflection, $variants, $hasSideEffects, - $stubPhpDocString, - $throwType + $throwType, ); } $declaringTraitName = $this->findMethodTrait($methodReflection); $resolvedPhpDoc = null; - $stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors($declaringClass, $methodReflection->getName(), array_map(static function (\ReflectionParameter $parameter): string { - return $parameter->getName(); - }, $methodReflection->getParameters())); + $stubPhpDocPair = $this->findMethodPhpDocIncludingAncestors($declaringClass, $methodReflection->getName(), array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $methodReflection->getParameters())); $phpDocBlockClassReflection = $declaringClass; if ($stubPhpDocPair !== null) { [$resolvedPhpDoc, $phpDocBlockClassReflection] = $stubPhpDocPair; } - $stubPhpDocString = null; if ($resolvedPhpDoc === null) { - if ($declaringClass->getFileName() !== false) { + if ($declaringClass->getFileName() !== null) { $docComment = $methodReflection->getDocComment(); - $positionalParameterNames = array_map(static function (\ReflectionParameter $parameter): string { - return $parameter->getName(); - }, $methodReflection->getParameters()); + $positionalParameterNames = array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $methodReflection->getParameters()); $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForMethod( $docComment, @@ -589,12 +579,10 @@ private function createMethod( $declaringClass, $declaringTraitName, $methodReflection->getName(), - $positionalParameterNames + $positionalParameterNames, ); $phpDocBlockClassReflection = $declaringClass; } - } else { - $stubPhpDocString = $resolvedPhpDoc->getPhpDocString(); } $declaringTrait = null; @@ -617,7 +605,7 @@ private function createMethod( if ( $methodReflection instanceof NativeBuiltinMethodReflection && $methodReflection->isConstructor() - && $declaringClass->getFileName() !== false + && $declaringClass->getFileName() !== null ) { foreach ($methodReflection->getParameters() as $parameter) { if (!method_exists($parameter, 'isPromoted') || !$parameter->isPromoted()) { @@ -641,7 +629,7 @@ private function createMethod( $declaringClassName, $declaringTraitName, $methodReflection->getName(), - $parameterProperty->getDocComment() + $parameterProperty->getDocComment(), ); $varTags = $propertyDocblock->getVarTags(); if (isset($varTags[0]) && count($varTags) === 1) { @@ -666,13 +654,13 @@ private function createMethod( foreach ($phpDocParameterTypes as $paramName => $paramType) { $phpDocParameterTypes[$paramName] = TemplateTypeHelper::resolveTemplateTypes( $paramType, - $phpDocBlockClassReflection->getActiveTemplateTypeMap() + $phpDocBlockClassReflection->getActiveTemplateTypeMap(), ); } $nativeReturnType = TypehintHelper::decideTypeFromReflection( $methodReflection->getReturnType(), null, - $declaringClass->getName() + $declaringClass->getName(), ); $phpDocReturnType = $this->getPhpDocReturnType($phpDocBlockClassReflection, $resolvedPhpDoc, $nativeReturnType); $phpDocThrowType = $resolvedPhpDoc->getThrowsTag() !== null ? $resolvedPhpDoc->getThrowsTag()->getType() : null; @@ -695,20 +683,15 @@ private function createMethod( $isDeprecated, $isInternal, $isFinal, - $stubPhpDocString, - $isPure + $isPure, ); } /** - * @param FunctionSignature $methodSignature * @param array $stubPhpDocParameterTypes * @param array $stubPhpDocParameterVariadicity - * @param Type|null $stubPhpDocReturnType * @param array $phpDocParameterTypes - * @param Type|null $phpDocReturnType * @param array $phpDocParameterNameMapping - * @return FunctionVariantWithPhpDocs */ private function createNativeMethodVariant( FunctionSignature $methodSignature, @@ -717,7 +700,7 @@ private function createNativeMethodVariant( ?Type $stubPhpDocReturnType, array $phpDocParameterTypes, ?Type $phpDocReturnType, - array $phpDocParameterNameMapping + array $phpDocParameterNameMapping, ): FunctionVariantWithPhpDocs { $parameters = []; @@ -742,7 +725,7 @@ private function createNativeMethodVariant( $parameterSignature->getNativeType(), $parameterSignature->passedByReference(), $stubPhpDocParameterVariadicity[$parameterSignature->getName()] ?? $parameterSignature->isVariadic(), - null + null, ); } @@ -759,7 +742,7 @@ private function createNativeMethodVariant( $methodSignature->isVariadic(), $returnType ?? $methodSignature->getReturnType(), $phpDocReturnType ?? new MixedType(), - $methodSignature->getNativeReturnType() + $methodSignature->getNativeReturnType(), ); } @@ -787,13 +770,11 @@ private function findPropertyTrait(\ReflectionProperty $propertyReflection): ?st } /** - * @param \ReflectionClass[] $traits - * @param \ReflectionProperty $propertyReflection - * @return string|null + * @param ReflectionClass[] $traits */ private function deepScanTraitsForProperty( array $traits, - \ReflectionProperty $propertyReflection + \ReflectionProperty $propertyReflection, ): ?string { foreach ($traits as $trait) { @@ -816,7 +797,7 @@ private function deepScanTraitsForProperty( } private function findMethodTrait( - BuiltinMethodReflection $methodReflection + BuiltinMethodReflection $methodReflection, ): ?string { if ($methodReflection->getReflection() instanceof ReflectionMethod) { @@ -865,10 +846,9 @@ private function findMethodTrait( } /** - * @param \ReflectionClass $class - * @return \ReflectionClass[] + * @return ReflectionClass[] */ - private function collectTraits(\ReflectionClass $class): array + private function collectTraits(ReflectionClass $class): array { $traits = []; $traitsLeftToAnalyze = $class->getTraits(); @@ -893,7 +873,7 @@ private function collectTraits(\ReflectionClass $class): array private function inferPrivatePropertyType( string $propertyName, - MethodReflection $constructor + MethodReflection $constructor, ): ?Type { $declaringClassName = $constructor->getDeclaringClass()->getName(); @@ -911,18 +891,17 @@ private function inferPrivatePropertyType( } /** - * @param \PHPStan\Reflection\MethodReflection $constructor * @return array */ private function inferAndCachePropertyTypes( - MethodReflection $constructor + MethodReflection $constructor, ): array { $declaringClass = $constructor->getDeclaringClass(); if (isset($this->propertyTypesCache[$declaringClass->getName()])) { return $this->propertyTypesCache[$declaringClass->getName()]; } - if ($declaringClass->getFileName() === false) { + if ($declaringClass->getFileName() === null) { return $this->propertyTypesCache[$declaringClass->getName()] = []; } @@ -934,7 +913,7 @@ private function inferAndCachePropertyTypes( } $methodNode = $this->findConstructorNode($constructor->getName(), $classNode->stmts); - if ($methodNode === null || $methodNode->stmts === null) { + if ($methodNode === null || $methodNode->stmts === null || count($methodNode->stmts) === 0) { return $this->propertyTypesCache[$declaringClass->getName()] = []; } @@ -949,7 +928,7 @@ private function inferAndCachePropertyTypes( false, [], $constructor, - $namespace + $namespace, )->enterClass($declaringClass); [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure] = $this->nodeScopeResolver->getPhpDocs($classScope, $methodNode); $methodScope = $classScope->enterClassMethod( @@ -962,7 +941,7 @@ private function inferAndCachePropertyTypes( $isDeprecated, $isInternal, $isFinal, - $isPure + $isPure, ); $propertyTypes = []; @@ -994,7 +973,7 @@ private function inferAndCachePropertyTypes( continue; } - $propertyType = TypeUtils::generalizeType($propertyType); + $propertyType = $propertyType->generalize(GeneralizePrecision::lessSpecific()); if ($propertyType instanceof ConstantArrayType) { $propertyType = new ArrayType(new MixedType(true), new MixedType(true)); } @@ -1006,15 +985,14 @@ private function inferAndCachePropertyTypes( } /** - * @param string $className - * @param \PhpParser\Node[] $nodes - * @return \PhpParser\Node\Stmt\Class_|null + * @param Node[] $nodes */ private function findClassNode(string $className, array $nodes): ?Class_ { foreach ($nodes as $node) { if ( $node instanceof Class_ + && $node->namespacedName !== null && $node->namespacedName->toString() === $className ) { return $node; @@ -1042,9 +1020,7 @@ private function findClassNode(string $className, array $nodes): ?Class_ } /** - * @param string $methodName - * @param \PhpParser\Node\Stmt[] $classStatements - * @return \PhpParser\Node\Stmt\ClassMethod|null + * @param Node\Stmt[] $classStatements */ private function findConstructorNode(string $methodName, array $classStatements): ?ClassMethod { @@ -1070,7 +1046,7 @@ private function getPhpDocReturnType(ClassReflection $phpDocBlockClassReflection $phpDocReturnType = $returnTag->getType(); $phpDocReturnType = TemplateTypeHelper::resolveTemplateTypes( $phpDocReturnType, - $phpDocBlockClassReflection->getActiveTemplateTypeMap() + $phpDocBlockClassReflection->getActiveTemplateTypeMap(), ); if ($returnTag->isExplicit() || $nativeReturnType->isSuperTypeOf($phpDocReturnType)->yes()) { @@ -1081,10 +1057,8 @@ private function getPhpDocReturnType(ClassReflection $phpDocBlockClassReflection } /** - * @param ClassReflection $declaringClass - * @param string $methodName * @param array $positionalParameterNames - * @return array{\PHPStan\PhpDoc\ResolvedPhpDocBlock, ClassReflection}|null + * @return array{ResolvedPhpDocBlock, ClassReflection}|null */ private function findMethodPhpDocIncludingAncestors(ClassReflection $declaringClass, string $methodName, array $positionalParameterNames): ?array { diff --git a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php index d1c109af91..cb4ec0b402 100644 --- a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php @@ -2,97 +2,60 @@ namespace PHPStan\Reflection\Php; +use PhpParser\Node; use PhpParser\Node\Expr\Variable; use PhpParser\Node\FunctionLike; use PhpParser\Node\Stmt\ClassMethod; +use PhpParser\Node\Stmt\Function_; +use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\FunctionVariantWithPhpDocs; +use PHPStan\Reflection\ParameterReflectionWithPhpDocs; +use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\Reflection\PassedByReference; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; use PHPStan\Type\VoidType; +use function array_reverse; +use function is_array; +use function is_string; -class PhpFunctionFromParserNodeReflection implements \PHPStan\Reflection\FunctionReflection +class PhpFunctionFromParserNodeReflection implements FunctionReflection { - private \PhpParser\Node\FunctionLike $functionLike; - - private \PHPStan\Type\Generic\TemplateTypeMap $templateTypeMap; - - /** @var \PHPStan\Type\Type[] */ - private array $realParameterTypes; - - /** @var \PHPStan\Type\Type[] */ - private array $phpDocParameterTypes; - - /** @var \PHPStan\Type\Type[] */ - private array $realParameterDefaultValues; - - private \PHPStan\Type\Type $realReturnType; - - private ?\PHPStan\Type\Type $phpDocReturnType; - - private ?\PHPStan\Type\Type $throwType; - - private ?string $deprecatedDescription; - - private bool $isDeprecated; - - private bool $isInternal; - - private bool $isFinal; - - private ?bool $isPure; + /** @var Function_|ClassMethod */ + private Node\FunctionLike $functionLike; /** @var FunctionVariantWithPhpDocs[]|null */ private ?array $variants = null; /** - * @param FunctionLike $functionLike - * @param TemplateTypeMap $templateTypeMap - * @param \PHPStan\Type\Type[] $realParameterTypes - * @param \PHPStan\Type\Type[] $phpDocParameterTypes - * @param \PHPStan\Type\Type[] $realParameterDefaultValues - * @param Type $realReturnType - * @param Type|null $phpDocReturnType - * @param Type|null $throwType - * @param string|null $deprecatedDescription - * @param bool $isDeprecated - * @param bool $isInternal - * @param bool $isFinal - * @param bool|null $isPure + * @param Function_|ClassMethod $functionLike + * @param Type[] $realParameterTypes + * @param Type[] $phpDocParameterTypes + * @param Type[] $realParameterDefaultValues */ public function __construct( FunctionLike $functionLike, - TemplateTypeMap $templateTypeMap, - array $realParameterTypes, - array $phpDocParameterTypes, - array $realParameterDefaultValues, - Type $realReturnType, - ?Type $phpDocReturnType = null, - ?Type $throwType = null, - ?string $deprecatedDescription = null, - bool $isDeprecated = false, - bool $isInternal = false, - bool $isFinal = false, - ?bool $isPure = null + private string $fileName, + private TemplateTypeMap $templateTypeMap, + private array $realParameterTypes, + private array $phpDocParameterTypes, + private array $realParameterDefaultValues, + private Type $realReturnType, + private ?Type $phpDocReturnType, + private ?Type $throwType, + private ?string $deprecatedDescription, + private bool $isDeprecated, + private bool $isInternal, + private bool $isFinal, + private ?bool $isPure, ) { $this->functionLike = $functionLike; - $this->templateTypeMap = $templateTypeMap; - $this->realParameterTypes = $realParameterTypes; - $this->phpDocParameterTypes = $phpDocParameterTypes; - $this->realParameterDefaultValues = $realParameterDefaultValues; - $this->realReturnType = $realReturnType; - $this->phpDocReturnType = $phpDocReturnType; - $this->throwType = $throwType; - $this->deprecatedDescription = $deprecatedDescription; - $this->isDeprecated = $isDeprecated; - $this->isInternal = $isInternal; - $this->isFinal = $isFinal; - $this->isPure = $isPure; } protected function getFunctionLike(): FunctionLike @@ -100,17 +63,26 @@ protected function getFunctionLike(): FunctionLike return $this->functionLike; } + public function getFileName(): string + { + return $this->fileName; + } + public function getName(): string { if ($this->functionLike instanceof ClassMethod) { return $this->functionLike->name->name; } + if ($this->functionLike->namespacedName === null) { + throw new ShouldNotHappenException(); + } + return (string) $this->functionLike->namespacedName; } /** - * @return \PHPStan\Reflection\ParametersAcceptorWithPhpDocs[] + * @return ParametersAcceptorWithPhpDocs[] */ public function getVariants(): array { @@ -123,7 +95,7 @@ public function getVariants(): array $this->isVariadic(), $this->getReturnType(), $this->phpDocReturnType ?? new MixedType(), - $this->realReturnType + $this->realReturnType, ), ]; } @@ -132,21 +104,21 @@ public function getVariants(): array } /** - * @return \PHPStan\Reflection\ParameterReflectionWithPhpDocs[] + * @return ParameterReflectionWithPhpDocs[] */ private function getParameters(): array { $parameters = []; $isOptional = true; - /** @var \PhpParser\Node\Param $parameter */ + /** @var Node\Param $parameter */ foreach (array_reverse($this->functionLike->getParams()) as $parameter) { if ($parameter->default === null && !$parameter->variadic) { $isOptional = false; } if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $parameters[] = new PhpParameterFromParserNodeReflection( $parameter->var->name, @@ -157,7 +129,7 @@ private function getParameters(): array ? PassedByReference::createCreatesNewVariable() : PassedByReference::createNo(), $this->realParameterDefaultValues[$parameter->var->name] ?? null, - $parameter->variadic + $parameter->variadic, ); } @@ -230,4 +202,40 @@ public function isBuiltin(): bool return false; } + public function isGenerator(): bool + { + return $this->nodeIsOrContainsYield($this->functionLike); + } + + private function nodeIsOrContainsYield(Node $node): bool + { + if ($node instanceof Node\Expr\Yield_) { + return true; + } + + if ($node instanceof Node\Expr\YieldFrom) { + return true; + } + + foreach ($node->getSubNodeNames() as $nodeName) { + $nodeProperty = $node->$nodeName; + + if ($nodeProperty instanceof Node && $this->nodeIsOrContainsYield($nodeProperty)) { + return true; + } + + if (!is_array($nodeProperty)) { + continue; + } + + foreach ($nodeProperty as $nodePropertyArrayItem) { + if ($nodePropertyArrayItem instanceof Node && $this->nodeIsOrContainsYield($nodePropertyArrayItem)) { + return true; + } + } + } + + return false; + } + } diff --git a/src/Reflection/Php/PhpFunctionReflection.php b/src/Reflection/Php/PhpFunctionReflection.php index f16327cfd5..fc53462093 100644 --- a/src/Reflection/Php/PhpFunctionReflection.php +++ b/src/Reflection/Php/PhpFunctionReflection.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection\Php; +use PhpParser\Node; use PhpParser\Node\Stmt\ClassLike; use PhpParser\Node\Stmt\Declare_; use PhpParser\Node\Stmt\Function_; @@ -11,99 +12,49 @@ use PHPStan\Parser\Parser; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\FunctionVariantWithPhpDocs; +use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; -use PHPStan\Reflection\ReflectionWithFilename; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; use PHPStan\Type\VoidType; - -class PhpFunctionReflection implements FunctionReflection, ReflectionWithFilename +use ReflectionFunction; +use ReflectionParameter; +use function array_map; +use function filemtime; +use function is_file; +use function sprintf; +use function time; + +class PhpFunctionReflection implements FunctionReflection { - private \ReflectionFunction $reflection; - - private \PHPStan\Parser\Parser $parser; - - private \PHPStan\Parser\FunctionCallStatementFinder $functionCallStatementFinder; - - private \PHPStan\Cache\Cache $cache; - - private \PHPStan\Type\Generic\TemplateTypeMap $templateTypeMap; - - /** @var \PHPStan\Type\Type[] */ - private array $phpDocParameterTypes; - - private ?\PHPStan\Type\Type $phpDocReturnType; - - private ?\PHPStan\Type\Type $phpDocThrowType; - - private ?string $deprecatedDescription; - - private bool $isDeprecated; - - private bool $isInternal; - - private bool $isFinal; - - /** @var string|false */ - private $filename; - - private ?bool $isPure; - /** @var FunctionVariantWithPhpDocs[]|null */ private ?array $variants = null; /** - * @param \ReflectionFunction $reflection - * @param Parser $parser - * @param FunctionCallStatementFinder $functionCallStatementFinder - * @param Cache $cache - * @param TemplateTypeMap $templateTypeMap - * @param \PHPStan\Type\Type[] $phpDocParameterTypes - * @param Type|null $phpDocReturnType - * @param Type|null $phpDocThrowType - * @param string|null $deprecatedDescription - * @param bool $isDeprecated - * @param bool $isInternal - * @param bool $isFinal - * @param string|false $filename - * @param bool|null $isPure + * @param Type[] $phpDocParameterTypes */ public function __construct( - \ReflectionFunction $reflection, - Parser $parser, - FunctionCallStatementFinder $functionCallStatementFinder, - Cache $cache, - TemplateTypeMap $templateTypeMap, - array $phpDocParameterTypes, - ?Type $phpDocReturnType, - ?Type $phpDocThrowType, - ?string $deprecatedDescription, - bool $isDeprecated, - bool $isInternal, - bool $isFinal, - $filename, - ?bool $isPure = null + private ReflectionFunction $reflection, + private Parser $parser, + private FunctionCallStatementFinder $functionCallStatementFinder, + private Cache $cache, + private TemplateTypeMap $templateTypeMap, + private array $phpDocParameterTypes, + private ?Type $phpDocReturnType, + private ?Type $phpDocThrowType, + private ?string $deprecatedDescription, + private bool $isDeprecated, + private bool $isInternal, + private bool $isFinal, + private ?string $filename, + private ?bool $isPure, ) { - $this->reflection = $reflection; - $this->parser = $parser; - $this->functionCallStatementFinder = $functionCallStatementFinder; - $this->cache = $cache; - $this->templateTypeMap = $templateTypeMap; - $this->phpDocParameterTypes = $phpDocParameterTypes; - $this->phpDocReturnType = $phpDocReturnType; - $this->phpDocThrowType = $phpDocThrowType; - $this->isDeprecated = $isDeprecated; - $this->deprecatedDescription = $deprecatedDescription; - $this->isInternal = $isInternal; - $this->isFinal = $isFinal; - $this->filename = $filename; - $this->isPure = $isPure; } public function getName(): string @@ -111,17 +62,14 @@ public function getName(): string return $this->reflection->getName(); } - /** - * @return string|false - */ - public function getFileName() + public function getFileName(): ?string { - if ($this->filename === false) { - return false; + if ($this->filename === null) { + return null; } - if (!file_exists($this->filename)) { - return false; + if (!is_file($this->filename)) { + return null; } return $this->filename; @@ -141,7 +89,7 @@ public function getVariants(): array $this->isVariadic(), $this->getReturnType(), $this->getPhpDocReturnType(), - $this->getNativeReturnType() + $this->getNativeReturnType(), ), ]; } @@ -150,17 +98,15 @@ public function getVariants(): array } /** - * @return \PHPStan\Reflection\ParameterReflectionWithPhpDocs[] + * @return ParameterReflectionWithPhpDocs[] */ private function getParameters(): array { - return array_map(function (\ReflectionParameter $reflection): PhpParameterReflection { - return new PhpParameterReflection( - $reflection, - $this->phpDocParameterTypes[$reflection->getName()] ?? null, - null - ); - }, $this->reflection->getParameters()); + return array_map(fn (ReflectionParameter $reflection): PhpParameterReflection => new PhpParameterReflection( + $reflection, + $this->phpDocParameterTypes[$reflection->getName()] ?? null, + null, + ), $this->reflection->getParameters()); } private function isVariadic(): bool @@ -168,13 +114,13 @@ private function isVariadic(): bool $isNativelyVariadic = $this->reflection->isVariadic(); if (!$isNativelyVariadic && $this->reflection->getFileName() !== false) { $fileName = $this->reflection->getFileName(); - if (file_exists($fileName)) { + if (is_file($fileName)) { $functionName = $this->reflection->getName(); $modifiedTime = filemtime($fileName); if ($modifiedTime === false) { $modifiedTime = time(); } - $variableCacheKey = sprintf('%d-v1', $modifiedTime); + $variableCacheKey = sprintf('%d-v3', $modifiedTime); $key = sprintf('variadic-function-%s-%s', $functionName, $fileName); $cachedResult = $this->cache->load($key, $variableCacheKey); if ($cachedResult === null) { @@ -192,8 +138,7 @@ private function isVariadic(): bool } /** - * @param \PhpParser\Node[] $nodes - * @return bool + * @param Node[] $nodes */ private function callsFuncGetArgs(array $nodes): bool { @@ -234,7 +179,7 @@ private function getReturnType(): Type { return TypehintHelper::decideTypeFromReflection( $this->reflection->getReturnType(), - $this->phpDocReturnType + $this->phpDocReturnType, ); } @@ -264,7 +209,7 @@ public function getDeprecatedDescription(): ?string public function isDeprecated(): TrinaryLogic { return TrinaryLogic::createFromBoolean( - $this->isDeprecated || $this->reflection->isDeprecated() + $this->isDeprecated || $this->reflection->isDeprecated(), ); } diff --git a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php index ee5f5d6dd8..26fa3a7833 100644 --- a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php @@ -2,10 +2,12 @@ namespace PHPStan\Reflection\Php; +use PhpParser\Node; use PhpParser\Node\Stmt\ClassMethod; use PHPStan\Reflection\ClassMemberReflection; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\MissingMethodFromReflectionException; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\Generic\TemplateTypeMap; @@ -15,31 +17,20 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\VoidType; +use function strtolower; class PhpMethodFromParserNodeReflection extends PhpFunctionFromParserNodeReflection implements MethodReflection { - private \PHPStan\Reflection\ClassReflection $declaringClass; - /** - * @param ClassReflection $declaringClass - * @param ClassMethod $classMethod - * @param TemplateTypeMap $templateTypeMap - * @param \PHPStan\Type\Type[] $realParameterTypes - * @param \PHPStan\Type\Type[] $phpDocParameterTypes - * @param \PHPStan\Type\Type[] $realParameterDefaultValues - * @param Type $realReturnType - * @param Type|null $phpDocReturnType - * @param Type|null $throwType - * @param string|null $deprecatedDescription - * @param bool $isDeprecated - * @param bool $isInternal - * @param bool $isFinal - * @param bool|null $isPure + * @param Type[] $realParameterTypes + * @param Type[] $phpDocParameterTypes + * @param Type[] $realParameterDefaultValues */ public function __construct( - ClassReflection $declaringClass, + private ClassReflection $declaringClass, ClassMethod $classMethod, + string $fileName, TemplateTypeMap $templateTypeMap, array $realParameterTypes, array $phpDocParameterTypes, @@ -51,7 +42,7 @@ public function __construct( bool $isDeprecated, bool $isInternal, bool $isFinal, - ?bool $isPure = null + ?bool $isPure, ) { $name = strtolower($classMethod->name->name); @@ -79,6 +70,7 @@ public function __construct( parent::__construct( $classMethod, + $fileName, $templateTypeMap, $realParameterTypes, $phpDocParameterTypes, @@ -90,9 +82,8 @@ public function __construct( $isDeprecated, $isInternal, $isFinal || $classMethod->isFinal(), - $isPure + $isPure, ); - $this->declaringClass = $declaringClass; } public function getDeclaringClass(): ClassReflection @@ -104,14 +95,14 @@ public function getPrototype(): ClassMemberReflection { try { return $this->declaringClass->getNativeMethod($this->getClassMethod()->name->name)->getPrototype(); - } catch (\PHPStan\Reflection\MissingMethodFromReflectionException $e) { + } catch (MissingMethodFromReflectionException) { return $this; } } private function getClassMethod(): ClassMethod { - /** @var \PhpParser\Node\Stmt\ClassMethod $functionLike */ + /** @var Node\Stmt\ClassMethod $functionLike */ $functionLike = $this->getFunctionLike(); return $functionLike; } diff --git a/src/Reflection/Php/PhpMethodReflection.php b/src/Reflection/Php/PhpMethodReflection.php index c6fab4535e..563793f82b 100644 --- a/src/Reflection/Php/PhpMethodReflection.php +++ b/src/Reflection/Php/PhpMethodReflection.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection\Php; +use PhpParser\Node; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Declare_; use PhpParser\Node\Stmt\Function_; @@ -14,6 +15,7 @@ use PHPStan\Reflection\FunctionVariantWithPhpDocs; use PHPStan\Reflection\MethodPrototypeReflection; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\Reflection\ReflectionProvider; @@ -28,110 +30,54 @@ use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; use PHPStan\Type\VoidType; +use ReflectionException; +use ReflectionParameter; +use function array_map; +use function explode; +use function filemtime; +use function is_bool; +use function is_file; +use function sprintf; +use function strtolower; +use function time; +use const PHP_VERSION_ID; /** @api */ class PhpMethodReflection implements MethodReflection { - private \PHPStan\Reflection\ClassReflection $declaringClass; - - private ?ClassReflection $declaringTrait; - - private BuiltinMethodReflection $reflection; - - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Parser\Parser $parser; - - private \PHPStan\Parser\FunctionCallStatementFinder $functionCallStatementFinder; - - private \PHPStan\Cache\Cache $cache; - - private \PHPStan\Type\Generic\TemplateTypeMap $templateTypeMap; - - /** @var \PHPStan\Type\Type[] */ - private array $phpDocParameterTypes; - - private ?\PHPStan\Type\Type $phpDocReturnType; - - private ?\PHPStan\Type\Type $phpDocThrowType; - - /** @var \PHPStan\Reflection\Php\PhpParameterReflection[]|null */ + /** @var PhpParameterReflection[]|null */ private ?array $parameters = null; - private ?\PHPStan\Type\Type $returnType = null; - - private ?\PHPStan\Type\Type $nativeReturnType = null; + private ?Type $returnType = null; - private ?string $deprecatedDescription; - - private bool $isDeprecated; - - private bool $isInternal; - - private bool $isFinal; - - private ?bool $isPure; - - private ?string $stubPhpDocString; + private ?Type $nativeReturnType = null; /** @var FunctionVariantWithPhpDocs[]|null */ private ?array $variants = null; /** - * @param ClassReflection $declaringClass - * @param ClassReflection|null $declaringTrait - * @param BuiltinMethodReflection $reflection - * @param \PHPStan\Reflection\ReflectionProvider $reflectionProvider - * @param Parser $parser - * @param FunctionCallStatementFinder $functionCallStatementFinder - * @param Cache $cache - * @param \PHPStan\Type\Type[] $phpDocParameterTypes - * @param Type|null $phpDocReturnType - * @param Type|null $phpDocThrowType - * @param string|null $deprecatedDescription - * @param bool $isDeprecated - * @param bool $isInternal - * @param bool $isFinal - * @param string|null $stubPhpDocString + * @param Type[] $phpDocParameterTypes */ public function __construct( - ClassReflection $declaringClass, - ?ClassReflection $declaringTrait, - BuiltinMethodReflection $reflection, - ReflectionProvider $reflectionProvider, - Parser $parser, - FunctionCallStatementFinder $functionCallStatementFinder, - Cache $cache, - TemplateTypeMap $templateTypeMap, - array $phpDocParameterTypes, - ?Type $phpDocReturnType, - ?Type $phpDocThrowType, - ?string $deprecatedDescription, - bool $isDeprecated, - bool $isInternal, - bool $isFinal, - ?string $stubPhpDocString, - ?bool $isPure = null + private ClassReflection $declaringClass, + private ?ClassReflection $declaringTrait, + private BuiltinMethodReflection $reflection, + private ReflectionProvider $reflectionProvider, + private Parser $parser, + private FunctionCallStatementFinder $functionCallStatementFinder, + private Cache $cache, + private TemplateTypeMap $templateTypeMap, + private array $phpDocParameterTypes, + private ?Type $phpDocReturnType, + private ?Type $phpDocThrowType, + private ?string $deprecatedDescription, + private bool $isDeprecated, + private bool $isInternal, + private bool $isFinal, + private ?bool $isPure, ) { - $this->declaringClass = $declaringClass; - $this->declaringTrait = $declaringTrait; - $this->reflection = $reflection; - $this->reflectionProvider = $reflectionProvider; - $this->parser = $parser; - $this->functionCallStatementFinder = $functionCallStatementFinder; - $this->cache = $cache; - $this->templateTypeMap = $templateTypeMap; - $this->phpDocParameterTypes = $phpDocParameterTypes; - $this->phpDocReturnType = $phpDocReturnType; - $this->phpDocThrowType = $phpDocThrowType; - $this->deprecatedDescription = $deprecatedDescription; - $this->isDeprecated = $isDeprecated; - $this->isInternal = $isInternal; - $this->isFinal = $isFinal; - $this->stubPhpDocString = $stubPhpDocString; - $this->isPure = $isPure; } public function getDeclaringClass(): ClassReflection @@ -146,15 +92,11 @@ public function getDeclaringTrait(): ?ClassReflection public function getDocComment(): ?string { - if ($this->stubPhpDocString !== null) { - return $this->stubPhpDocString; - } - return $this->reflection->getDocComment(); } /** - * @return self|\PHPStan\Reflection\MethodPrototypeReflection + * @return self|MethodPrototypeReflection */ public function getPrototype(): ClassMemberReflection { @@ -162,6 +104,11 @@ public function getPrototype(): ClassMemberReflection $prototypeMethod = $this->reflection->getPrototype(); $prototypeDeclaringClass = $this->reflectionProvider->getClass($prototypeMethod->getDeclaringClass()->getName()); + $tentativeReturnType = null; + if ($prototypeMethod->getTentativeReturnType() !== null) { + $tentativeReturnType = TypehintHelper::decideTypeFromReflection($prototypeMethod->getTentativeReturnType()); + } + return new MethodPrototypeReflection( $prototypeMethod->getName(), $prototypeDeclaringClass, @@ -170,9 +117,10 @@ public function getPrototype(): ClassMemberReflection $prototypeMethod->isPublic(), $prototypeMethod->isAbstract(), $prototypeMethod->isFinal(), - $prototypeDeclaringClass->getNativeMethod($prototypeMethod->getName())->getVariants() + $prototypeDeclaringClass->getNativeMethod($prototypeMethod->getName())->getVariants(), + $tentativeReturnType, ); - } catch (\ReflectionException $e) { + } catch (ReflectionException) { return $this; } } @@ -187,6 +135,10 @@ public function getName(): string $name = $this->reflection->getName(); $lowercaseName = strtolower($name); if ($lowercaseName === $name) { + if (PHP_VERSION_ID >= 80000) { + return $name; + } + // fix for https://bugs.php.net/bug.php?id=74939 foreach ($this->getDeclaringClass()->getNativeReflection()->getTraitAliases() as $traitTarget) { $correctName = $this->getMethodNameWithCorrectCase($name, $traitTarget); @@ -232,7 +184,7 @@ public function getVariants(): array $this->isVariadic(), $this->getReturnType(), $this->getPhpDocReturnType(), - $this->getNativeReturnType() + $this->getNativeReturnType(), ), ]; } @@ -241,18 +193,16 @@ public function getVariants(): array } /** - * @return \PHPStan\Reflection\ParameterReflectionWithPhpDocs[] + * @return ParameterReflectionWithPhpDocs[] */ private function getParameters(): array { if ($this->parameters === null) { - $this->parameters = array_map(function (\ReflectionParameter $reflection): PhpParameterReflection { - return new PhpParameterReflection( - $reflection, - $this->phpDocParameterTypes[$reflection->getName()] ?? null, - $this->getDeclaringClass()->getName() - ); - }, $this->reflection->getParameters()); + $this->parameters = array_map(fn (ReflectionParameter $reflection): PhpParameterReflection => new PhpParameterReflection( + $reflection, + $this->phpDocParameterTypes[$reflection->getName()] ?? null, + $this->getDeclaringClass()->getName(), + ), $this->reflection->getParameters()); } return $this->parameters; @@ -268,13 +218,13 @@ private function isVariadic(): bool $filename = $this->declaringTrait->getFileName(); } - if (!$isNativelyVariadic && $filename !== false && file_exists($filename)) { + if (!$isNativelyVariadic && $filename !== null && is_file($filename)) { $modifiedTime = filemtime($filename); if ($modifiedTime === false) { $modifiedTime = time(); } $key = sprintf('variadic-method-%s-%s-%s', $declaringClass->getName(), $this->reflection->getName(), $filename); - $variableCacheKey = sprintf('%d-v2', $modifiedTime); + $variableCacheKey = sprintf('%d-v4', $modifiedTime); $cachedResult = $this->cache->load($key, $variableCacheKey); if ($cachedResult === null || !is_bool($cachedResult)) { $nodes = $this->parser->parseFile($filename); @@ -290,15 +240,13 @@ private function isVariadic(): bool } /** - * @param ClassReflection $declaringClass - * @param \PhpParser\Node[] $nodes - * @return bool + * @param Node[] $nodes */ private function callsFuncGetArgs(ClassReflection $declaringClass, array $nodes): bool { foreach ($nodes as $node) { if ( - $node instanceof \PhpParser\Node\Stmt\ClassLike + $node instanceof Node\Stmt\ClassLike ) { if (!isset($node->namespacedName)) { continue; @@ -387,7 +335,7 @@ private function getReturnType(): Type $this->returnType = TypehintHelper::decideTypeFromReflection( $this->reflection->getReturnType(), $this->phpDocReturnType, - $this->declaringClass->getName() + $this->declaringClass->getName(), ); } @@ -409,7 +357,7 @@ private function getNativeReturnType(): Type $this->nativeReturnType = TypehintHelper::decideTypeFromReflection( $this->reflection->getReturnType(), null, - $this->declaringClass->getName() + $this->declaringClass->getName(), ); } diff --git a/src/Reflection/Php/PhpMethodReflectionFactory.php b/src/Reflection/Php/PhpMethodReflectionFactory.php index 4d06c19570..8b56043ac5 100644 --- a/src/Reflection/Php/PhpMethodReflectionFactory.php +++ b/src/Reflection/Php/PhpMethodReflectionFactory.php @@ -10,21 +10,8 @@ interface PhpMethodReflectionFactory { /** - * @param \PHPStan\Reflection\ClassReflection $declaringClass - * @param \PHPStan\Reflection\ClassReflection|null $declaringTrait - * @param BuiltinMethodReflection $reflection - * @param TemplateTypeMap $templateTypeMap - * @param \PHPStan\Type\Type[] $phpDocParameterTypes - * @param \PHPStan\Type\Type|null $phpDocReturnType - * @param \PHPStan\Type\Type|null $phpDocThrowType - * @param string|null $deprecatedDescription - * @param bool $isDeprecated - * @param bool $isInternal - * @param bool $isFinal - * @param string|null $stubPhpDocString - * @param bool|null $isPure + * @param Type[] $phpDocParameterTypes * - * @return \PHPStan\Reflection\Php\PhpMethodReflection */ public function create( ClassReflection $declaringClass, @@ -38,8 +25,7 @@ public function create( bool $isDeprecated, bool $isInternal, bool $isFinal, - ?string $stubPhpDocString, - ?bool $isPure = null + ?bool $isPure = null, ): PhpMethodReflection; } diff --git a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php index 09c17d2a46..0625921242 100644 --- a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php @@ -2,48 +2,29 @@ namespace PHPStan\Reflection\Php; +use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\PassedByReference; use PHPStan\Type\MixedType; use PHPStan\Type\NullType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypehintHelper; -class PhpParameterFromParserNodeReflection implements \PHPStan\Reflection\ParameterReflectionWithPhpDocs +class PhpParameterFromParserNodeReflection implements ParameterReflectionWithPhpDocs { - private string $name; - - private bool $optional; - - private \PHPStan\Type\Type $realType; - - private ?\PHPStan\Type\Type $phpDocType; - - private \PHPStan\Reflection\PassedByReference $passedByReference; - - private ?\PHPStan\Type\Type $defaultValue; - - private bool $variadic; - - private ?\PHPStan\Type\Type $type = null; + private ?Type $type = null; public function __construct( - string $name, - bool $optional, - Type $realType, - ?Type $phpDocType, - PassedByReference $passedByReference, - ?Type $defaultValue, - bool $variadic + private string $name, + private bool $optional, + private Type $realType, + private ?Type $phpDocType, + private PassedByReference $passedByReference, + private ?Type $defaultValue, + private bool $variadic, ) { - $this->name = $name; - $this->optional = $optional; - $this->realType = $realType; - $this->phpDocType = $phpDocType; - $this->passedByReference = $passedByReference; - $this->defaultValue = $defaultValue; - $this->variadic = $variadic; } public function getName(): string @@ -64,7 +45,7 @@ public function getType(): Type if ($this->defaultValue instanceof NullType) { $inferred = $phpDocType->inferTemplateTypes($this->defaultValue); if ($inferred->isEmpty()) { - $phpDocType = \PHPStan\Type\TypeCombinator::addNull($phpDocType); + $phpDocType = TypeCombinator::addNull($phpDocType); } } } diff --git a/src/Reflection/Php/PhpParameterReflection.php b/src/Reflection/Php/PhpParameterReflection.php index c134d7a1b2..35dc2e8216 100644 --- a/src/Reflection/Php/PhpParameterReflection.php +++ b/src/Reflection/Php/PhpParameterReflection.php @@ -7,30 +7,24 @@ use PHPStan\Type\ConstantTypeHelper; use PHPStan\Type\MixedType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypehintHelper; +use ReflectionParameter; +use Throwable; class PhpParameterReflection implements ParameterReflectionWithPhpDocs { - private \ReflectionParameter $reflection; + private ?Type $type = null; - private ?\PHPStan\Type\Type $phpDocType; - - private ?\PHPStan\Type\Type $type = null; - - private ?\PHPStan\Type\Type $nativeType = null; - - private ?string $declaringClassName; + private ?Type $nativeType = null; public function __construct( - \ReflectionParameter $reflection, - ?Type $phpDocType, - ?string $declaringClassName + private ReflectionParameter $reflection, + private ?Type $phpDocType, + private ?string $declaringClassName, ) { - $this->reflection = $reflection; - $this->phpDocType = $phpDocType; - $this->declaringClassName = $declaringClassName; } public function isOptional(): bool @@ -49,10 +43,13 @@ public function getType(): Type $phpDocType = $this->phpDocType; if ($phpDocType !== null) { try { - if ($this->reflection->isDefaultValueAvailable() && $this->reflection->getDefaultValue() === null) { - $phpDocType = \PHPStan\Type\TypeCombinator::addNull($phpDocType); + if ( + $this->reflection->isDefaultValueAvailable() + && $this->reflection->getDefaultValue() === null + ) { + $phpDocType = TypeCombinator::addNull($phpDocType); } - } catch (\Throwable $e) { + } catch (Throwable) { // pass } } @@ -61,7 +58,7 @@ public function getType(): Type $this->reflection->getType(), $phpDocType, $this->declaringClassName, - $this->isVariadic() + $this->isVariadic(), ); } @@ -96,7 +93,7 @@ public function getNativeType(): Type $this->reflection->getType(), null, $this->declaringClassName, - $this->isVariadic() + $this->isVariadic(), ); } @@ -110,7 +107,7 @@ public function getDefaultValue(): ?Type $defaultValue = $this->reflection->getDefaultValue(); return ConstantTypeHelper::getTypeFromValue($defaultValue); } - } catch (\Throwable $e) { + } catch (Throwable) { return null; } diff --git a/src/Reflection/Php/PhpPropertyReflection.php b/src/Reflection/Php/PhpPropertyReflection.php index d173ba72fd..3e0fab20c1 100644 --- a/src/Reflection/Php/PhpPropertyReflection.php +++ b/src/Reflection/Php/PhpPropertyReflection.php @@ -8,54 +8,29 @@ use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; +use ReflectionProperty; +use ReflectionType; +use function method_exists; /** @api */ class PhpPropertyReflection implements PropertyReflection { - private \PHPStan\Reflection\ClassReflection $declaringClass; + private ?Type $finalNativeType = null; - private ?\PHPStan\Reflection\ClassReflection $declaringTrait; - - private ?\ReflectionType $nativeType; - - private ?\PHPStan\Type\Type $finalNativeType = null; - - private ?\PHPStan\Type\Type $phpDocType; - - private ?\PHPStan\Type\Type $type = null; - - private \ReflectionProperty $reflection; - - private ?string $deprecatedDescription; - - private bool $isDeprecated; - - private bool $isInternal; - - private ?string $stubPhpDocString; + private ?Type $type = null; public function __construct( - ClassReflection $declaringClass, - ?ClassReflection $declaringTrait, - ?\ReflectionType $nativeType, - ?Type $phpDocType, - \ReflectionProperty $reflection, - ?string $deprecatedDescription, - bool $isDeprecated, - bool $isInternal, - ?string $stubPhpDocString + private ClassReflection $declaringClass, + private ?ClassReflection $declaringTrait, + private ?ReflectionType $nativeType, + private ?Type $phpDocType, + private ReflectionProperty $reflection, + private ?string $deprecatedDescription, + private bool $isDeprecated, + private bool $isInternal, ) { - $this->declaringClass = $declaringClass; - $this->declaringTrait = $declaringTrait; - $this->nativeType = $nativeType; - $this->phpDocType = $phpDocType; - $this->reflection = $reflection; - $this->deprecatedDescription = $deprecatedDescription; - $this->isDeprecated = $isDeprecated; - $this->isInternal = $isInternal; - $this->stubPhpDocString = $stubPhpDocString; } public function getDeclaringClass(): ClassReflection @@ -70,10 +45,6 @@ public function getDeclaringTrait(): ?ClassReflection public function getDocComment(): ?string { - if ($this->stubPhpDocString !== null) { - return $this->stubPhpDocString; - } - $docComment = $this->reflection->getDocComment(); if ($docComment === false) { return null; @@ -97,13 +68,22 @@ public function isPublic(): bool return $this->reflection->isPublic(); } + public function isReadOnly(): bool + { + if (method_exists($this->reflection, 'isReadOnly')) { + return $this->reflection->isReadOnly(); + } + + return false; + } + public function getReadableType(): Type { if ($this->type === null) { $this->type = TypehintHelper::decideTypeFromReflection( $this->nativeType, $this->phpDocType, - $this->declaringClass->getName() + $this->declaringClass->getName(), ); } @@ -129,7 +109,7 @@ public function isPromoted(): bool return $this->reflection->isPromoted(); } - public function hasPhpDoc(): bool + public function hasPhpDocType(): bool { return $this->phpDocType !== null; } @@ -143,13 +123,18 @@ public function getPhpDocType(): Type return new MixedType(); } + public function hasNativeType(): bool + { + return $this->nativeType !== null; + } + public function getNativeType(): Type { if ($this->finalNativeType === null) { $this->finalNativeType = TypehintHelper::decideTypeFromReflection( $this->nativeType, null, - $this->declaringClass->getName() + $this->declaringClass->getName(), ); } @@ -185,7 +170,7 @@ public function isInternal(): TrinaryLogic return TrinaryLogic::createFromBoolean($this->isInternal); } - public function getNativeReflection(): \ReflectionProperty + public function getNativeReflection(): ReflectionProperty { return $this->reflection; } diff --git a/src/Reflection/Php/SimpleXMLElementProperty.php b/src/Reflection/Php/SimpleXMLElementProperty.php index 098e62a820..9668d523be 100644 --- a/src/Reflection/Php/SimpleXMLElementProperty.php +++ b/src/Reflection/Php/SimpleXMLElementProperty.php @@ -15,17 +15,11 @@ class SimpleXMLElementProperty implements PropertyReflection { - private \PHPStan\Reflection\ClassReflection $declaringClass; - - private \PHPStan\Type\Type $type; - public function __construct( - ClassReflection $declaringClass, - Type $type + private ClassReflection $declaringClass, + private Type $type, ) { - $this->declaringClass = $declaringClass; - $this->type = $type; } public function getDeclaringClass(): ClassReflection @@ -60,7 +54,7 @@ public function getWritableType(): Type new IntegerType(), new FloatType(), new StringType(), - new BooleanType() + new BooleanType(), ); } diff --git a/src/Reflection/Php/Soap/SoapClientMethodReflection.php b/src/Reflection/Php/Soap/SoapClientMethodReflection.php index 32701b305e..15a814b20a 100644 --- a/src/Reflection/Php/Soap/SoapClientMethodReflection.php +++ b/src/Reflection/Php/Soap/SoapClientMethodReflection.php @@ -15,14 +15,8 @@ class SoapClientMethodReflection implements MethodReflection { - private ClassReflection $declaringClass; - - private string $name; - - public function __construct(ClassReflection $declaringClass, string $name) + public function __construct(private ClassReflection $declaringClass, private string $name) { - $this->declaringClass = $declaringClass; - $this->name = $name; } public function getDeclaringClass(): ClassReflection @@ -68,7 +62,7 @@ public function getVariants(): array TemplateTypeMap::createEmpty(), [], true, - new MixedType(true) + new MixedType(true), ), ]; } diff --git a/src/Reflection/Php/UniversalObjectCrateProperty.php b/src/Reflection/Php/UniversalObjectCrateProperty.php index 5dbad67143..3d766f5fee 100644 --- a/src/Reflection/Php/UniversalObjectCrateProperty.php +++ b/src/Reflection/Php/UniversalObjectCrateProperty.php @@ -3,27 +3,19 @@ namespace PHPStan\Reflection\Php; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\PropertyReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; -class UniversalObjectCrateProperty implements \PHPStan\Reflection\PropertyReflection +class UniversalObjectCrateProperty implements PropertyReflection { - private \PHPStan\Reflection\ClassReflection $declaringClass; - - private \PHPStan\Type\Type $readableType; - - private \PHPStan\Type\Type $writableType; - public function __construct( - ClassReflection $declaringClass, - Type $readableType, - Type $writableType + private ClassReflection $declaringClass, + private Type $readableType, + private Type $writableType, ) { - $this->declaringClass = $declaringClass; - $this->readableType = $readableType; - $this->writableType = $writableType; } public function getDeclaringClass(): ClassReflection diff --git a/src/Reflection/Php/UniversalObjectCratesClassReflectionExtension.php b/src/Reflection/Php/UniversalObjectCratesClassReflectionExtension.php index 8c0ec93652..6260882e02 100644 --- a/src/Reflection/Php/UniversalObjectCratesClassReflectionExtension.php +++ b/src/Reflection/Php/UniversalObjectCratesClassReflectionExtension.php @@ -2,54 +2,40 @@ namespace PHPStan\Reflection\Php; -use PHPStan\Broker\Broker; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\PropertiesClassReflectionExtension; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\MixedType; class UniversalObjectCratesClassReflectionExtension - implements \PHPStan\Reflection\PropertiesClassReflectionExtension, \PHPStan\Reflection\BrokerAwareExtension + implements PropertiesClassReflectionExtension { - /** @var string[] */ - private array $classes; - - private \PHPStan\Broker\Broker $broker; - /** * @param string[] $classes */ - public function __construct(array $classes) - { - $this->classes = $classes; - } - - public function setBroker(Broker $broker): void + public function __construct(private ReflectionProvider $reflectionProvider, private array $classes) { - $this->broker = $broker; } public function hasProperty(ClassReflection $classReflection, string $propertyName): bool { return self::isUniversalObjectCrate( - $this->broker, + $this->reflectionProvider, $this->classes, - $classReflection + $classReflection, ); } /** - * @param \PHPStan\Reflection\ReflectionProvider $reflectionProvider * @param string[] $classes - * @param \PHPStan\Reflection\ClassReflection $classReflection - * @return bool */ public static function isUniversalObjectCrate( ReflectionProvider $reflectionProvider, array $classes, - ClassReflection $classReflection + ClassReflection $classReflection, ): bool { foreach ($classes as $className) { diff --git a/src/Reflection/ReflectionProvider.php b/src/Reflection/ReflectionProvider.php index 12b0041386..a41202567b 100644 --- a/src/Reflection/ReflectionProvider.php +++ b/src/Reflection/ReflectionProvider.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection; +use PhpParser\Node; use PHPStan\Analyser\Scope; /** @api */ @@ -17,20 +18,20 @@ public function getClassName(string $className): string; public function supportsAnonymousClasses(): bool; public function getAnonymousClassReflection( - \PhpParser\Node\Stmt\Class_ $classNode, - Scope $scope + Node\Stmt\Class_ $classNode, + Scope $scope, ): ClassReflection; - public function hasFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool; + public function hasFunction(Node\Name $nameNode, ?Scope $scope): bool; - public function getFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): FunctionReflection; + public function getFunction(Node\Name $nameNode, ?Scope $scope): FunctionReflection; - public function resolveFunctionName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string; + public function resolveFunctionName(Node\Name $nameNode, ?Scope $scope): ?string; - public function hasConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool; + public function hasConstant(Node\Name $nameNode, ?Scope $scope): bool; - public function getConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection; + public function getConstant(Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection; - public function resolveConstantName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string; + public function resolveConstantName(Node\Name $nameNode, ?Scope $scope): ?string; } diff --git a/src/Reflection/ReflectionProvider/ChainReflectionProvider.php b/src/Reflection/ReflectionProvider/ChainReflectionProvider.php index c2502f5efb..f8b1183c68 100644 --- a/src/Reflection/ReflectionProvider/ChainReflectionProvider.php +++ b/src/Reflection/ReflectionProvider/ChainReflectionProvider.php @@ -2,26 +2,27 @@ namespace PHPStan\Reflection\ReflectionProvider; +use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Broker\ClassNotFoundException; +use PHPStan\Broker\ConstantNotFoundException; +use PHPStan\Broker\FunctionNotFoundException; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\GlobalConstantReflection; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\ShouldNotHappenException; class ChainReflectionProvider implements ReflectionProvider { - /** @var \PHPStan\Reflection\ReflectionProvider[] */ - private array $providers; - /** - * @param \PHPStan\Reflection\ReflectionProvider[] $providers + * @param ReflectionProvider[] $providers */ public function __construct( - array $providers + private array $providers, ) { - $this->providers = $providers; } public function hasClass(string $className): bool @@ -47,7 +48,7 @@ public function getClass(string $className): ClassReflection return $provider->getClass($className); } - throw new \PHPStan\Broker\ClassNotFoundException($className); + throw new ClassNotFoundException($className); } public function getClassName(string $className): string @@ -60,7 +61,7 @@ public function getClassName(string $className): string return $provider->getClassName($className); } - throw new \PHPStan\Broker\ClassNotFoundException($className); + throw new ClassNotFoundException($className); } public function supportsAnonymousClasses(): bool @@ -76,7 +77,7 @@ public function supportsAnonymousClasses(): bool return false; } - public function getAnonymousClassReflection(\PhpParser\Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection + public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection { foreach ($this->providers as $provider) { if (!$provider->supportsAnonymousClasses()) { @@ -86,10 +87,10 @@ public function getAnonymousClassReflection(\PhpParser\Node\Stmt\Class_ $classNo return $provider->getAnonymousClassReflection($classNode, $scope); } - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - public function hasFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool + public function hasFunction(Node\Name $nameNode, ?Scope $scope): bool { foreach ($this->providers as $provider) { if (!$provider->hasFunction($nameNode, $scope)) { @@ -102,7 +103,7 @@ public function hasFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool return false; } - public function getFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): FunctionReflection + public function getFunction(Node\Name $nameNode, ?Scope $scope): FunctionReflection { foreach ($this->providers as $provider) { if (!$provider->hasFunction($nameNode, $scope)) { @@ -112,10 +113,10 @@ public function getFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): Func return $provider->getFunction($nameNode, $scope); } - throw new \PHPStan\Broker\FunctionNotFoundException((string) $nameNode); + throw new FunctionNotFoundException((string) $nameNode); } - public function resolveFunctionName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string + public function resolveFunctionName(Node\Name $nameNode, ?Scope $scope): ?string { foreach ($this->providers as $provider) { $resolvedName = $provider->resolveFunctionName($nameNode, $scope); @@ -129,7 +130,7 @@ public function resolveFunctionName(\PhpParser\Node\Name $nameNode, ?Scope $scop return null; } - public function hasConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool + public function hasConstant(Node\Name $nameNode, ?Scope $scope): bool { foreach ($this->providers as $provider) { if (!$provider->hasConstant($nameNode, $scope)) { @@ -142,7 +143,7 @@ public function hasConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool return false; } - public function getConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection + public function getConstant(Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection { foreach ($this->providers as $provider) { if (!$provider->hasConstant($nameNode, $scope)) { @@ -152,10 +153,10 @@ public function getConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): Glob return $provider->getConstant($nameNode, $scope); } - throw new \PHPStan\Broker\ConstantNotFoundException((string) $nameNode); + throw new ConstantNotFoundException((string) $nameNode); } - public function resolveConstantName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string + public function resolveConstantName(Node\Name $nameNode, ?Scope $scope): ?string { foreach ($this->providers as $provider) { $resolvedName = $provider->resolveConstantName($nameNode, $scope); diff --git a/src/Reflection/ReflectionProvider/ClassBlacklistReflectionProvider.php b/src/Reflection/ReflectionProvider/ClassBlacklistReflectionProvider.php index b3ace3ff3f..734b9f5636 100644 --- a/src/Reflection/ReflectionProvider/ClassBlacklistReflectionProvider.php +++ b/src/Reflection/ReflectionProvider/ClassBlacklistReflectionProvider.php @@ -3,41 +3,31 @@ namespace PHPStan\Reflection\ReflectionProvider; use Nette\Utils\Strings; +use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; +use PHPStan\Broker\ClassNotFoundException; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\GlobalConstantReflection; use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Reflection\ReflectionWithFilename; +use PHPStan\ShouldNotHappenException; +use ReflectionClass; +use function class_exists; class ClassBlacklistReflectionProvider implements ReflectionProvider { - private ReflectionProvider $reflectionProvider; - - private PhpStormStubsSourceStubber $phpStormStubsSourceStubber; - - /** @var string[] */ - private array $patterns; - - private ?string $singleReflectionInsteadOfFile; - /** - * @param \PHPStan\Reflection\ReflectionProvider $reflectionProvider * @param string[] $patterns */ public function __construct( - ReflectionProvider $reflectionProvider, - PhpStormStubsSourceStubber $phpStormStubsSourceStubber, - array $patterns, - ?string $singleReflectionInsteadOfFile + private ReflectionProvider $reflectionProvider, + private PhpStormStubsSourceStubber $phpStormStubsSourceStubber, + private array $patterns, + private ?string $singleReflectionInsteadOfFile, ) { - $this->reflectionProvider = $reflectionProvider; - $this->phpStormStubsSourceStubber = $phpStormStubsSourceStubber; - $this->patterns = $patterns; - $this->singleReflectionInsteadOfFile = $singleReflectionInsteadOfFile; } public function hasClass(string $className): bool @@ -80,10 +70,7 @@ private function isClassBlacklisted(string $className): bool if (!class_exists($className, false)) { return true; } - if (in_array(strtolower($className), ['reflectionuniontype', 'attribute'], true)) { - return true; - } - $reflection = new \ReflectionClass($className); + $reflection = new ReflectionClass($className); if ($reflection->getFileName() === false) { return true; } @@ -101,7 +88,7 @@ private function isClassBlacklisted(string $className): bool public function getClass(string $className): ClassReflection { if (!$this->hasClass($className)) { - throw new \PHPStan\Broker\ClassNotFoundException($className); + throw new ClassNotFoundException($className); } return $this->reflectionProvider->getClass($className); @@ -110,7 +97,7 @@ public function getClass(string $className): ClassReflection public function getClassName(string $className): string { if (!$this->hasClass($className)) { - throw new \PHPStan\Broker\ClassNotFoundException($className); + throw new ClassNotFoundException($className); } return $this->reflectionProvider->getClassName($className); @@ -121,12 +108,12 @@ public function supportsAnonymousClasses(): bool return false; } - public function getAnonymousClassReflection(\PhpParser\Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection + public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - public function hasFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool + public function hasFunction(Node\Name $nameNode, ?Scope $scope): bool { $has = $this->reflectionProvider->hasFunction($nameNode, $scope); if (!$has) { @@ -138,34 +125,31 @@ public function hasFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool } $functionReflection = $this->reflectionProvider->getFunction($nameNode, $scope); - if (!$functionReflection instanceof ReflectionWithFilename) { - return true; - } return $functionReflection->getFileName() !== $this->singleReflectionInsteadOfFile; } - public function getFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): FunctionReflection + public function getFunction(Node\Name $nameNode, ?Scope $scope): FunctionReflection { return $this->reflectionProvider->getFunction($nameNode, $scope); } - public function resolveFunctionName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string + public function resolveFunctionName(Node\Name $nameNode, ?Scope $scope): ?string { return $this->reflectionProvider->resolveFunctionName($nameNode, $scope); } - public function hasConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool + public function hasConstant(Node\Name $nameNode, ?Scope $scope): bool { return $this->reflectionProvider->hasConstant($nameNode, $scope); } - public function getConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection + public function getConstant(Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection { return $this->reflectionProvider->getConstant($nameNode, $scope); } - public function resolveConstantName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string + public function resolveConstantName(Node\Name $nameNode, ?Scope $scope): ?string { return $this->reflectionProvider->resolveConstantName($nameNode, $scope); } diff --git a/src/Reflection/ReflectionProvider/DirectReflectionProviderProvider.php b/src/Reflection/ReflectionProvider/DirectReflectionProviderProvider.php index 96b4dd2fe0..e1c7e31e1f 100644 --- a/src/Reflection/ReflectionProvider/DirectReflectionProviderProvider.php +++ b/src/Reflection/ReflectionProvider/DirectReflectionProviderProvider.php @@ -7,11 +7,8 @@ class DirectReflectionProviderProvider implements ReflectionProviderProvider { - private ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct(private ReflectionProvider $reflectionProvider) { - $this->reflectionProvider = $reflectionProvider; } public function getReflectionProvider(): ReflectionProvider diff --git a/src/Reflection/ReflectionProvider/DummyReflectionProvider.php b/src/Reflection/ReflectionProvider/DummyReflectionProvider.php new file mode 100644 index 0000000000..9af3f4539d --- /dev/null +++ b/src/Reflection/ReflectionProvider/DummyReflectionProvider.php @@ -0,0 +1,71 @@ +container = $container; } public function getReflectionProvider(): ReflectionProvider diff --git a/src/Reflection/ReflectionProvider/MemoizingReflectionProvider.php b/src/Reflection/ReflectionProvider/MemoizingReflectionProvider.php index 751cb5160d..1cb6f44eb9 100644 --- a/src/Reflection/ReflectionProvider/MemoizingReflectionProvider.php +++ b/src/Reflection/ReflectionProvider/MemoizingReflectionProvider.php @@ -2,39 +2,37 @@ namespace PHPStan\Reflection\ReflectionProvider; +use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\GlobalConstantReflection; use PHPStan\Reflection\ReflectionProvider; +use function strtolower; class MemoizingReflectionProvider implements ReflectionProvider { - private \PHPStan\Reflection\ReflectionProvider $provider; - /** @var array */ private array $hasClasses = []; - /** @var array */ + /** @var array */ private array $classes = []; /** @var array */ private array $classNames = []; - public function __construct(ReflectionProvider $provider) + public function __construct(private ReflectionProvider $provider) { - $this->provider = $provider; } public function hasClass(string $className): bool { - $lowerClassName = strtolower($className); - if (isset($this->hasClasses[$lowerClassName])) { - return $this->hasClasses[$lowerClassName]; + if (isset($this->hasClasses[$className])) { + return $this->hasClasses[$className]; } - return $this->hasClasses[$lowerClassName] = $this->provider->hasClass($className); + return $this->hasClasses[$className] = $this->provider->hasClass($className); } public function getClass(string $className): ClassReflection @@ -62,37 +60,37 @@ public function supportsAnonymousClasses(): bool return $this->provider->supportsAnonymousClasses(); } - public function getAnonymousClassReflection(\PhpParser\Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection + public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $scope): ClassReflection { return $this->provider->getAnonymousClassReflection($classNode, $scope); } - public function hasFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool + public function hasFunction(Node\Name $nameNode, ?Scope $scope): bool { return $this->provider->hasFunction($nameNode, $scope); } - public function getFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): FunctionReflection + public function getFunction(Node\Name $nameNode, ?Scope $scope): FunctionReflection { return $this->provider->getFunction($nameNode, $scope); } - public function resolveFunctionName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string + public function resolveFunctionName(Node\Name $nameNode, ?Scope $scope): ?string { return $this->provider->resolveFunctionName($nameNode, $scope); } - public function hasConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool + public function hasConstant(Node\Name $nameNode, ?Scope $scope): bool { return $this->provider->hasConstant($nameNode, $scope); } - public function getConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection + public function getConstant(Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection { return $this->provider->getConstant($nameNode, $scope); } - public function resolveConstantName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string + public function resolveConstantName(Node\Name $nameNode, ?Scope $scope): ?string { return $this->provider->resolveConstantName($nameNode, $scope); } diff --git a/src/Reflection/ReflectionProvider/ReflectionProviderFactory.php b/src/Reflection/ReflectionProvider/ReflectionProviderFactory.php index cc0d3cb35e..9f54ac31b3 100644 --- a/src/Reflection/ReflectionProvider/ReflectionProviderFactory.php +++ b/src/Reflection/ReflectionProvider/ReflectionProviderFactory.php @@ -3,25 +3,17 @@ namespace PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\ReflectionProvider; +use function count; class ReflectionProviderFactory { - private \PHPStan\Reflection\ReflectionProvider $runtimeReflectionProvider; - - private \PHPStan\Reflection\ReflectionProvider $staticReflectionProvider; - - private bool $disableRuntimeReflectionProvider; - public function __construct( - ReflectionProvider $runtimeReflectionProvider, - ReflectionProvider $staticReflectionProvider, - bool $disableRuntimeReflectionProvider + private ReflectionProvider $runtimeReflectionProvider, + private ReflectionProvider $staticReflectionProvider, + private bool $disableRuntimeReflectionProvider, ) { - $this->runtimeReflectionProvider = $runtimeReflectionProvider; - $this->staticReflectionProvider = $staticReflectionProvider; - $this->disableRuntimeReflectionProvider = $disableRuntimeReflectionProvider; } public function create(): ReflectionProvider diff --git a/src/Reflection/ReflectionProviderStaticAccessor.php b/src/Reflection/ReflectionProviderStaticAccessor.php new file mode 100644 index 0000000000..bb772064c7 --- /dev/null +++ b/src/Reflection/ReflectionProviderStaticAccessor.php @@ -0,0 +1,29 @@ +parametersAcceptor = $parametersAcceptor; - $this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap; } public function getOriginalParametersAcceptor(): ParametersAcceptor @@ -48,16 +43,14 @@ public function getParameters(): array $parameters = $this->parameters; if ($parameters === null) { - $parameters = array_map(function (ParameterReflection $param): ParameterReflection { - return new DummyParameter( - $param->getName(), - TemplateTypeHelper::resolveTemplateTypes($param->getType(), $this->resolvedTemplateTypeMap), - $param->isOptional(), - $param->passedByReference(), - $param->isVariadic(), - $param->getDefaultValue() - ); - }, $this->parametersAcceptor->getParameters()); + $parameters = array_map(fn (ParameterReflection $param): ParameterReflection => new DummyParameter( + $param->getName(), + TemplateTypeHelper::resolveTemplateTypes($param->getType(), $this->resolvedTemplateTypeMap), + $param->isOptional(), + $param->passedByReference(), + $param->isVariadic(), + $param->getDefaultValue(), + ), $this->parametersAcceptor->getParameters()); $this->parameters = $parameters; } @@ -77,7 +70,7 @@ public function getReturnType(): Type if ($type === null) { $type = TemplateTypeHelper::resolveTemplateTypes( $this->parametersAcceptor->getReturnType(), - $this->resolvedTemplateTypeMap + $this->resolvedTemplateTypeMap, ); $this->returnType = $type; diff --git a/src/Reflection/ResolvedMethodReflection.php b/src/Reflection/ResolvedMethodReflection.php index 77e03b7f4d..eeab6cab92 100644 --- a/src/Reflection/ResolvedMethodReflection.php +++ b/src/Reflection/ResolvedMethodReflection.php @@ -10,17 +10,11 @@ class ResolvedMethodReflection implements MethodReflection { - private MethodReflection $reflection; - - private TemplateTypeMap $resolvedTemplateTypeMap; - - /** @var \PHPStan\Reflection\ParametersAcceptor[]|null */ + /** @var ParametersAcceptor[]|null */ private ?array $variants = null; - public function __construct(MethodReflection $reflection, TemplateTypeMap $resolvedTemplateTypeMap) + public function __construct(private MethodReflection $reflection, private TemplateTypeMap $resolvedTemplateTypeMap) { - $this->reflection = $reflection; - $this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap; } public function getName(): string @@ -34,7 +28,7 @@ public function getPrototype(): ClassMemberReflection } /** - * @return \PHPStan\Reflection\ParametersAcceptor[] + * @return ParametersAcceptor[] */ public function getVariants(): array { @@ -47,7 +41,7 @@ public function getVariants(): array foreach ($this->reflection->getVariants() as $variant) { $variants[] = new ResolvedFunctionVariant( $variant, - $this->resolvedTemplateTypeMap + $this->resolvedTemplateTypeMap, ); } diff --git a/src/Reflection/ResolvedPropertyReflection.php b/src/Reflection/ResolvedPropertyReflection.php index f75c267964..0596d61d6b 100644 --- a/src/Reflection/ResolvedPropertyReflection.php +++ b/src/Reflection/ResolvedPropertyReflection.php @@ -11,18 +11,12 @@ class ResolvedPropertyReflection implements WrapperPropertyReflection { - private PropertyReflection $reflection; - - private TemplateTypeMap $templateTypeMap; - private ?Type $readableType = null; private ?Type $writableType = null; - public function __construct(PropertyReflection $reflection, TemplateTypeMap $templateTypeMap) + public function __construct(private PropertyReflection $reflection, private TemplateTypeMap $templateTypeMap) { - $this->reflection = $reflection; - $this->templateTypeMap = $templateTypeMap; } public function getOriginalReflection(): PropertyReflection @@ -68,11 +62,11 @@ public function getReadableType(): Type $type = TemplateTypeHelper::resolveTemplateTypes( $this->reflection->getReadableType(), - $this->templateTypeMap + $this->templateTypeMap, ); $type = TemplateTypeHelper::resolveTemplateTypes( $type, - $this->templateTypeMap + $this->templateTypeMap, ); $this->readableType = $type; @@ -89,11 +83,11 @@ public function getWritableType(): Type $type = TemplateTypeHelper::resolveTemplateTypes( $this->reflection->getWritableType(), - $this->templateTypeMap + $this->templateTypeMap, ); $type = TemplateTypeHelper::resolveTemplateTypes( $type, - $this->templateTypeMap + $this->templateTypeMap, ); $this->writableType = $type; diff --git a/src/Reflection/Runtime/RuntimeReflectionProvider.php b/src/Reflection/Runtime/RuntimeReflectionProvider.php index 7696136a6f..faf34914cb 100644 --- a/src/Reflection/Runtime/RuntimeReflectionProvider.php +++ b/src/Reflection/Runtime/RuntimeReflectionProvider.php @@ -2,87 +2,94 @@ namespace PHPStan\Reflection\Runtime; +use Closure; use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; +use PHPStan\Broker\ClassAutoloadingException; +use PHPStan\Broker\ClassNotFoundException; +use PHPStan\Broker\ConstantNotFoundException; +use PHPStan\Broker\FunctionNotFoundException; use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider; use PHPStan\Php\PhpVersion; +use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\PhpDoc\StubPhpDocProvider; use PHPStan\PhpDoc\Tag\ParamTag; use PHPStan\Reflection\ClassNameHelper; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\Constant\RuntimeConstantReflection; +use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\FunctionReflectionFactory; use PHPStan\Reflection\GlobalConstantReflection; +use PHPStan\Reflection\Php\PhpFunctionReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\SignatureMap\NativeFunctionReflectionProvider; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\ConstantTypeHelper; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Type; use ReflectionClass; +use ReflectionFunction; +use ReflectionParameter; +use Throwable; +use function array_key_exists; +use function array_map; +use function class_exists; +use function constant; +use function debug_backtrace; +use function defined; +use function function_exists; +use function interface_exists; +use function spl_autoload_register; +use function spl_autoload_unregister; +use function sprintf; +use function strtolower; +use function trait_exists; +use function trim; +use const DEBUG_BACKTRACE_IGNORE_ARGS; class RuntimeReflectionProvider implements ReflectionProvider { - private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider; - - private ClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider; - - /** @var \PHPStan\Reflection\ClassReflection[] */ + /** @var ClassReflection[] */ private array $classReflections = []; - private \PHPStan\Reflection\FunctionReflectionFactory $functionReflectionFactory; - - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - private PhpVersion $phpVersion; - - private \PHPStan\Reflection\SignatureMap\NativeFunctionReflectionProvider $nativeFunctionReflectionProvider; - - private StubPhpDocProvider $stubPhpDocProvider; - - private PhpStormStubsSourceStubber $phpStormStubsSourceStubber; - - /** @var \PHPStan\Reflection\FunctionReflection[] */ + /** @var FunctionReflection[] */ private array $functionReflections = []; - /** @var \PHPStan\Reflection\Php\PhpFunctionReflection[] */ + /** @var PhpFunctionReflection[] */ private array $customFunctionReflections = []; /** @var bool[] */ private array $hasClassCache = []; - /** @var \PHPStan\Reflection\ClassReflection[] */ + /** @var ClassReflection[] */ private static array $anonymousClasses = []; + /** @var array */ + private array $cachedConstants = []; + public function __construct( - ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, - ClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider, - FunctionReflectionFactory $functionReflectionFactory, - FileTypeMapper $fileTypeMapper, - PhpVersion $phpVersion, - NativeFunctionReflectionProvider $nativeFunctionReflectionProvider, - StubPhpDocProvider $stubPhpDocProvider, - PhpStormStubsSourceStubber $phpStormStubsSourceStubber + private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, + private ClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider, + private FunctionReflectionFactory $functionReflectionFactory, + private FileTypeMapper $fileTypeMapper, + private PhpDocInheritanceResolver $phpDocInheritanceResolver, + private PhpVersion $phpVersion, + private NativeFunctionReflectionProvider $nativeFunctionReflectionProvider, + private StubPhpDocProvider $stubPhpDocProvider, + private PhpStormStubsSourceStubber $phpStormStubsSourceStubber, ) { - $this->reflectionProviderProvider = $reflectionProviderProvider; - $this->classReflectionExtensionRegistryProvider = $classReflectionExtensionRegistryProvider; - $this->functionReflectionFactory = $functionReflectionFactory; - $this->fileTypeMapper = $fileTypeMapper; - $this->phpVersion = $phpVersion; - $this->nativeFunctionReflectionProvider = $nativeFunctionReflectionProvider; - $this->stubPhpDocProvider = $stubPhpDocProvider; - $this->phpStormStubsSourceStubber = $phpStormStubsSourceStubber; } - public function getClass(string $className): \PHPStan\Reflection\ClassReflection + public function getClass(string $className): ClassReflection { /** @var class-string $className */ $className = $className; if (!$this->hasClass($className)) { - throw new \PHPStan\Broker\ClassNotFoundException($className); + throw new ClassNotFoundException($className); } if (isset(self::$anonymousClasses[$className])) { @@ -99,7 +106,7 @@ public function getClass(string $className): \PHPStan\Reflection\ClassReflection $classReflection = $this->getClassFromReflection( $reflectionClass, $reflectionClass->getName(), - $reflectionClass->isAnonymous() ? $filename : null + $reflectionClass->isAnonymous() ? $filename : null, ); $this->classReflections[$className] = $classReflection; if ($className !== $reflectionClass->getName()) { @@ -114,7 +121,7 @@ public function getClass(string $className): \PHPStan\Reflection\ClassReflection public function getClassName(string $className): string { if (!$this->hasClass($className)) { - throw new \PHPStan\Broker\ClassNotFoundException($className); + throw new ClassNotFoundException($className); } /** @var class-string $className */ @@ -135,25 +142,25 @@ public function supportsAnonymousClasses(): bool } public function getAnonymousClassReflection( - \PhpParser\Node\Stmt\Class_ $classNode, - Scope $scope + Node\Stmt\Class_ $classNode, + Scope $scope, ): ClassReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } /** - * @param \ReflectionClass $reflectionClass - * @param string $displayName - * @param string|null $anonymousFilename + * @param ReflectionClass $reflectionClass */ - private function getClassFromReflection(\ReflectionClass $reflectionClass, string $displayName, ?string $anonymousFilename): ClassReflection + private function getClassFromReflection(ReflectionClass $reflectionClass, string $displayName, ?string $anonymousFilename): ClassReflection { $className = $reflectionClass->getName(); if (!isset($this->classReflections[$className])) { $classReflection = new ClassReflection( $this->reflectionProviderProvider->getReflectionProvider(), $this->fileTypeMapper, + $this->stubPhpDocProvider, + $this->phpDocInheritanceResolver, $this->phpVersion, $this->classReflectionExtensionRegistryProvider->getRegistry()->getPropertiesClassReflectionExtensions(), $this->classReflectionExtensionRegistryProvider->getRegistry()->getMethodsClassReflectionExtensions(), @@ -161,7 +168,7 @@ private function getClassFromReflection(\ReflectionClass $reflectionClass, strin $reflectionClass, $anonymousFilename, null, - $this->stubPhpDocProvider->findClassPhpDoc($className) + $this->stubPhpDocProvider->findClassPhpDoc($className), ); $this->classReflections[$className] = $classReflection; } @@ -183,29 +190,29 @@ public function hasClass(string $className): bool spl_autoload_register($autoloader = function (string $autoloadedClassName) use ($className): void { $autoloadedClassName = trim($autoloadedClassName, '\\'); if ($autoloadedClassName !== $className && !$this->isExistsCheckCall()) { - throw new \PHPStan\Broker\ClassAutoloadingException($autoloadedClassName); + throw new ClassAutoloadingException($autoloadedClassName); } }); try { return $this->hasClassCache[$className] = class_exists($className) || interface_exists($className) || trait_exists($className); - } catch (\PHPStan\Broker\ClassAutoloadingException $e) { + } catch (ClassAutoloadingException $e) { throw $e; - } catch (\Throwable $t) { - throw new \PHPStan\Broker\ClassAutoloadingException( + } catch (Throwable $t) { + throw new ClassAutoloadingException( $className, - $t + $t, ); } finally { spl_autoload_unregister($autoloader); } } - public function getFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): \PHPStan\Reflection\FunctionReflection + public function getFunction(Node\Name $nameNode, ?Scope $scope): FunctionReflection { $functionName = $this->resolveFunctionName($nameNode, $scope); if ($functionName === null) { - throw new \PHPStan\Broker\FunctionNotFoundException((string) $nameNode); + throw new FunctionNotFoundException((string) $nameNode); } $lowerCasedFunctionName = strtolower($functionName); @@ -224,12 +231,12 @@ public function getFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): \PHP return $this->functionReflections[$lowerCasedFunctionName]; } - public function hasFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool + public function hasFunction(Node\Name $nameNode, ?Scope $scope): bool { return $this->resolveFunctionName($nameNode, $scope) !== null; } - private function hasCustomFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool + private function hasCustomFunction(Node\Name $nameNode, ?Scope $scope): bool { $functionName = $this->resolveFunctionName($nameNode, $scope); if ($functionName === null) { @@ -239,23 +246,23 @@ private function hasCustomFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope return $this->nativeFunctionReflectionProvider->findFunctionReflection($functionName) === null; } - private function getCustomFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope): \PHPStan\Reflection\Php\PhpFunctionReflection + private function getCustomFunction(Node\Name $nameNode, ?Scope $scope): PhpFunctionReflection { if (!$this->hasCustomFunction($nameNode, $scope)) { - throw new \PHPStan\Broker\FunctionNotFoundException((string) $nameNode); + throw new FunctionNotFoundException((string) $nameNode); } /** @var string $functionName */ $functionName = $this->resolveFunctionName($nameNode, $scope); if (!function_exists($functionName)) { - throw new \PHPStan\Broker\FunctionNotFoundException($functionName); + throw new FunctionNotFoundException($functionName); } $lowerCasedFunctionName = strtolower($functionName); if (isset($this->customFunctionReflections[$lowerCasedFunctionName])) { return $this->customFunctionReflections[$lowerCasedFunctionName]; } - $reflectionFunction = new \ReflectionFunction($functionName); + $reflectionFunction = new ReflectionFunction($functionName); $templateTypeMap = TemplateTypeMap::createEmpty(); $phpDocParameterTags = []; $phpDocReturnTag = null; @@ -265,9 +272,7 @@ private function getCustomFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope $isInternal = false; $isFinal = false; $isPure = null; - $resolvedPhpDoc = $this->stubPhpDocProvider->findFunctionPhpDoc($reflectionFunction->getName(), array_map(static function (\ReflectionParameter $parameter): string { - return $parameter->getName(); - }, $reflectionFunction->getParameters())); + $resolvedPhpDoc = $this->stubPhpDocProvider->findFunctionPhpDoc($reflectionFunction->getName(), array_map(static fn (ReflectionParameter $parameter): string => $parameter->getName(), $reflectionFunction->getParameters())); if ($resolvedPhpDoc === null && $reflectionFunction->getFileName() !== false && $reflectionFunction->getDocComment() !== false) { $fileName = $reflectionFunction->getFileName(); $docComment = $reflectionFunction->getDocComment(); @@ -289,24 +294,22 @@ private function getCustomFunction(\PhpParser\Node\Name $nameNode, ?Scope $scope $functionReflection = $this->functionReflectionFactory->create( $reflectionFunction, $templateTypeMap, - array_map(static function (ParamTag $paramTag): Type { - return $paramTag->getType(); - }, $phpDocParameterTags), + array_map(static fn (ParamTag $paramTag): Type => $paramTag->getType(), $phpDocParameterTags), $phpDocReturnTag !== null ? $phpDocReturnTag->getType() : null, $phpDocThrowsTag !== null ? $phpDocThrowsTag->getType() : null, $deprecatedTag !== null ? $deprecatedTag->getMessage() : null, $isDeprecated, $isInternal, $isFinal, - $reflectionFunction->getFileName(), - $isPure + $reflectionFunction->getFileName() !== false ? $reflectionFunction->getFileName() : null, + $isPure, ); $this->customFunctionReflections[$lowerCasedFunctionName] = $functionReflection; return $functionReflection; } - public function resolveFunctionName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string + public function resolveFunctionName(Node\Name $nameNode, ?Scope $scope): ?string { return $this->resolveName($nameNode, function (string $name): bool { $exists = function_exists($name) || $this->nativeFunctionReflectionProvider->findFunctionReflection($name) !== null; @@ -322,42 +325,41 @@ public function resolveFunctionName(\PhpParser\Node\Name $nameNode, ?Scope $scop }, $scope); } - public function hasConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): bool + public function hasConstant(Node\Name $nameNode, ?Scope $scope): bool { return $this->resolveConstantName($nameNode, $scope) !== null; } - public function getConstant(\PhpParser\Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection + public function getConstant(Node\Name $nameNode, ?Scope $scope): GlobalConstantReflection { $constantName = $this->resolveConstantName($nameNode, $scope); if ($constantName === null) { - throw new \PHPStan\Broker\ConstantNotFoundException((string) $nameNode); + throw new ConstantNotFoundException((string) $nameNode); } - return new RuntimeConstantReflection( + if (array_key_exists($constantName, $this->cachedConstants)) { + return $this->cachedConstants[$constantName]; + } + + return $this->cachedConstants[$constantName] = new RuntimeConstantReflection( $constantName, - ConstantTypeHelper::getTypeFromValue(constant($constantName)), - null + ConstantTypeHelper::getTypeFromValue(@constant($constantName)), + null, ); } - public function resolveConstantName(\PhpParser\Node\Name $nameNode, ?Scope $scope): ?string + public function resolveConstantName(Node\Name $nameNode, ?Scope $scope): ?string { - return $this->resolveName($nameNode, static function (string $name): bool { - return defined($name); - }, $scope); + return $this->resolveName($nameNode, static fn (string $name): bool => defined($name), $scope); } /** - * @param Node\Name $nameNode - * @param \Closure(string $name): bool $existsCallback - * @param Scope|null $scope - * @return string|null + * @param Closure(string $name): bool $existsCallback */ private function resolveName( - \PhpParser\Node\Name $nameNode, - \Closure $existsCallback, - ?Scope $scope + Node\Name $nameNode, + Closure $existsCallback, + ?Scope $scope, ): ?string { $name = (string) $nameNode; @@ -386,8 +388,7 @@ private function isExistsCheckCall(): bool foreach ($debugBacktrace as $traceStep) { if ( - isset($traceStep['function']) - && isset($existsCallTypes[$traceStep['function']]) + isset($existsCallTypes[$traceStep['function']]) // We must ignore the self::hasClass calls && (!isset($traceStep['file']) || $traceStep['file'] !== __FILE__) ) { diff --git a/src/Reflection/SignatureMap/FunctionSignature.php b/src/Reflection/SignatureMap/FunctionSignature.php index 2068335c5f..8473684acc 100644 --- a/src/Reflection/SignatureMap/FunctionSignature.php +++ b/src/Reflection/SignatureMap/FunctionSignature.php @@ -7,36 +7,20 @@ class FunctionSignature { - /** @var \PHPStan\Reflection\SignatureMap\ParameterSignature[] */ - private array $parameters; - - private \PHPStan\Type\Type $returnType; - - private \PHPStan\Type\Type $nativeReturnType; - - private bool $variadic; - /** - * @param array $parameters - * @param \PHPStan\Type\Type $returnType - * @param \PHPStan\Type\Type $nativeReturnType - * @param bool $variadic + * @param array $parameters */ public function __construct( - array $parameters, - Type $returnType, - Type $nativeReturnType, - bool $variadic + private array $parameters, + private Type $returnType, + private Type $nativeReturnType, + private bool $variadic, ) { - $this->parameters = $parameters; - $this->returnType = $returnType; - $this->nativeReturnType = $nativeReturnType; - $this->variadic = $variadic; } /** - * @return array + * @return array */ public function getParameters(): array { diff --git a/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php b/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php index 0cdb5b1a21..6afcff805f 100644 --- a/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php +++ b/src/Reflection/SignatureMap/FunctionSignatureMapProvider.php @@ -3,26 +3,29 @@ namespace PHPStan\Reflection\SignatureMap; use PHPStan\Php\PhpVersion; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\MixedType; use PHPStan\Type\TypehintHelper; +use ReflectionMethod; +use function array_change_key_case; +use function array_key_exists; +use function array_keys; +use function is_array; +use function sprintf; +use function strtolower; +use const CASE_LOWER; class FunctionSignatureMapProvider implements SignatureMapProvider { - private \PHPStan\Reflection\SignatureMap\SignatureMapParser $parser; - - private PhpVersion $phpVersion; - /** @var mixed[]|null */ private ?array $signatureMap = null; /** @var array|null */ private ?array $functionMetadata = null; - public function __construct(SignatureMapParser $parser, PhpVersion $phpVersion) + public function __construct(private SignatureMapParser $parser, private PhpVersion $phpVersion) { - $this->parser = $parser; - $this->phpVersion = $phpVersion; } public function hasMethodSignature(string $className, string $methodName, int $variant = 0): bool @@ -39,7 +42,7 @@ public function hasFunctionSignature(string $name, int $variant = 0): bool return array_key_exists(strtolower($name), $signatureMap); } - public function getMethodSignature(string $className, string $methodName, ?\ReflectionMethod $reflectionMethod, int $variant = 0): FunctionSignature + public function getMethodSignature(string $className, string $methodName, ?ReflectionMethod $reflectionMethod, int $variant = 0): FunctionSignature { $signature = $this->getFunctionSignature(sprintf('%s::%s', $className, $methodName), $className, $variant); $parameters = []; @@ -60,7 +63,7 @@ public function getMethodSignature(string $className, string $methodName, ?\Refl $parameter->getType(), TypehintHelper::decideTypeFromReflection($nativeParameters[$i]->getType()), $parameter->passedByReference(), - $parameter->isVariadic() + $parameter->isVariadic(), ); } @@ -74,7 +77,7 @@ public function getMethodSignature(string $className, string $methodName, ?\Refl $parameters, $signature->getReturnType(), $nativeReturnType, - $signature->isVariadic() + $signature->isVariadic(), ); } @@ -86,14 +89,14 @@ public function getFunctionSignature(string $functionName, ?string $className, i } if (!$this->hasFunctionSignature($functionName)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $signatureMap = self::getSignatureMap(); return $this->parser->getFunctionSignature( $signatureMap[$functionName], - $className + $className, ); } @@ -109,8 +112,6 @@ public function hasFunctionMetadata(string $name): bool } /** - * @param string $className - * @param string $methodName * @return array{hasSideEffects: bool} */ public function getMethodMetadata(string $className, string $methodName): array @@ -119,7 +120,6 @@ public function getMethodMetadata(string $className, string $methodName): array } /** - * @param string $functionName * @return array{hasSideEffects: bool} */ public function getFunctionMetadata(string $functionName): array @@ -127,7 +127,7 @@ public function getFunctionMetadata(string $functionName): array $functionName = strtolower($functionName); if (!$this->hasFunctionMetadata($functionName)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $this->getFunctionMetadataMap()[$functionName]; @@ -155,7 +155,7 @@ public function getSignatureMap(): array if ($this->signatureMap === null) { $signatureMap = require __DIR__ . '/../../../resources/functionMap.php'; if (!is_array($signatureMap)) { - throw new \PHPStan\ShouldNotHappenException('Signature map could not be loaded.'); + throw new ShouldNotHappenException('Signature map could not be loaded.'); } $signatureMap = array_change_key_case($signatureMap, CASE_LOWER); @@ -163,7 +163,7 @@ public function getSignatureMap(): array if ($this->phpVersion->getVersionId() >= 70400) { $php74MapDelta = require __DIR__ . '/../../../resources/functionMap_php74delta.php'; if (!is_array($php74MapDelta)) { - throw new \PHPStan\ShouldNotHappenException('Signature map could not be loaded.'); + throw new ShouldNotHappenException('Signature map could not be loaded.'); } $signatureMap = $this->computeSignatureMap($signatureMap, $php74MapDelta); @@ -172,7 +172,7 @@ public function getSignatureMap(): array if ($this->phpVersion->getVersionId() >= 80000) { $php80MapDelta = require __DIR__ . '/../../../resources/functionMap_php80delta.php'; if (!is_array($php80MapDelta)) { - throw new \PHPStan\ShouldNotHappenException('Signature map could not be loaded.'); + throw new ShouldNotHappenException('Signature map could not be loaded.'); } $signatureMap = $this->computeSignatureMap($signatureMap, $php80MapDelta); diff --git a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php index 2e466e1f8c..fbd1794848 100644 --- a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php +++ b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php @@ -3,7 +3,8 @@ namespace PHPStan\Reflection\SignatureMap; use PHPStan\BetterReflection\Identifier\Exception\InvalidIdentifierName; -use PHPStan\BetterReflection\Reflector\FunctionReflector; +use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; +use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\PhpDoc\StubPhpDocProvider; use PHPStan\Reflection\FunctionVariant; @@ -22,6 +23,8 @@ use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; use PHPStan\Type\UnionType; +use function array_map; +use function strtolower; class NativeFunctionReflectionProvider { @@ -29,20 +32,8 @@ class NativeFunctionReflectionProvider /** @var NativeFunctionReflection[] */ private static array $functionMap = []; - private \PHPStan\Reflection\SignatureMap\SignatureMapProvider $signatureMapProvider; - - private \PHPStan\BetterReflection\Reflector\FunctionReflector $functionReflector; - - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - private StubPhpDocProvider $stubPhpDocProvider; - - public function __construct(SignatureMapProvider $signatureMapProvider, FunctionReflector $functionReflector, FileTypeMapper $fileTypeMapper, StubPhpDocProvider $stubPhpDocProvider) + public function __construct(private SignatureMapProvider $signatureMapProvider, private Reflector $reflector, private FileTypeMapper $fileTypeMapper, private StubPhpDocProvider $stubPhpDocProvider) { - $this->signatureMapProvider = $signatureMapProvider; - $this->functionReflector = $functionReflector; - $this->fileTypeMapper = $fileTypeMapper; - $this->stubPhpDocProvider = $stubPhpDocProvider; } public function findFunctionReflection(string $functionName): ?NativeFunctionReflection @@ -57,9 +48,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef } $reflectionFunction = $this->signatureMapProvider->getFunctionSignature($lowerCasedFunctionName, null); - $phpDoc = $this->stubPhpDocProvider->findFunctionPhpDoc($lowerCasedFunctionName, array_map(static function (ParameterSignature $parameter): string { - return $parameter->getName(); - }, $reflectionFunction->getParameters())); + $phpDoc = $this->stubPhpDocProvider->findFunctionPhpDoc($lowerCasedFunctionName, array_map(static fn (ParameterSignature $parameter): string => $parameter->getName(), $reflectionFunction->getParameters())); $variants = []; $i = 0; @@ -110,7 +99,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef new FloatType(), new NullType(), new BooleanType(), - ]) + ]), ); } @@ -127,11 +116,11 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef TypehintHelper::decideType($type, $phpDocType), $parameterSignature->passedByReference(), $parameterSignature->isVariadic(), - $defaultValue + $defaultValue, ); }, $functionSignature->getParameters()), $functionSignature->isVariadic(), - TypehintHelper::decideType($functionSignature->getReturnType(), $phpDoc !== null ? $this->getReturnTypeFromPhpDoc($phpDoc) : null) + TypehintHelper::decideType($functionSignature->getReturnType(), $phpDoc !== null ? $this->getReturnTypeFromPhpDoc($phpDoc) : null), ); $i++; @@ -144,8 +133,9 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef } $throwType = null; + $isDeprecated = false; try { - $reflectionFunction = $this->functionReflector->reflect($functionName); + $reflectionFunction = $this->reflector->reflectFunction($functionName); if ($reflectionFunction->getFileName() !== null) { $fileName = $reflectionFunction->getFileName(); $docComment = $reflectionFunction->getDocComment(); @@ -154,10 +144,11 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef if ($throwsTag !== null) { $throwType = $throwsTag->getType(); } + $isDeprecated = $reflectionFunction->isDeprecated(); } - } catch (\PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound $e) { + } catch (IdentifierNotFound) { // pass - } catch (InvalidIdentifierName $e) { + } catch (InvalidIdentifierName) { // pass } @@ -165,7 +156,8 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $lowerCasedFunctionName, $variants, $throwType, - $hasSideEffects + $hasSideEffects, + $isDeprecated, ); self::$functionMap[$lowerCasedFunctionName] = $functionReflection; diff --git a/src/Reflection/SignatureMap/ParameterSignature.php b/src/Reflection/SignatureMap/ParameterSignature.php index d4fc818292..45b8f5f294 100644 --- a/src/Reflection/SignatureMap/ParameterSignature.php +++ b/src/Reflection/SignatureMap/ParameterSignature.php @@ -8,33 +8,15 @@ class ParameterSignature { - private string $name; - - private bool $optional; - - private \PHPStan\Type\Type $type; - - private \PHPStan\Type\Type $nativeType; - - private \PHPStan\Reflection\PassedByReference $passedByReference; - - private bool $variadic; - public function __construct( - string $name, - bool $optional, - Type $type, - Type $nativeType, - PassedByReference $passedByReference, - bool $variadic + private string $name, + private bool $optional, + private Type $type, + private Type $nativeType, + private PassedByReference $passedByReference, + private bool $variadic, ) { - $this->name = $name; - $this->optional = $optional; - $this->type = $type; - $this->nativeType = $nativeType; - $this->passedByReference = $passedByReference; - $this->variadic = $variadic; } public function getName(): string diff --git a/src/Reflection/SignatureMap/Php8SignatureMapProvider.php b/src/Reflection/SignatureMap/Php8SignatureMapProvider.php index 86be50426a..9e2fd564d0 100644 --- a/src/Reflection/SignatureMap/Php8SignatureMapProvider.php +++ b/src/Reflection/SignatureMap/Php8SignatureMapProvider.php @@ -10,36 +10,34 @@ use PHPStan\PhpDoc\Tag\ParamTag; use PHPStan\Reflection\BetterReflection\SourceLocator\FileNodesFetcher; use PHPStan\Reflection\PassedByReference; -use PHPStan\Type\ArrayType; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\MixedType; use PHPStan\Type\ParserNodeTypeToPHPStanType; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; +use ReflectionMethod; +use function array_key_exists; +use function array_map; +use function count; +use function is_string; +use function sprintf; +use function strtolower; class Php8SignatureMapProvider implements SignatureMapProvider { private const DIRECTORY = __DIR__ . '/../../../vendor/phpstan/php-8-stubs'; - private FunctionSignatureMapProvider $functionSignatureMapProvider; - - private FileNodesFetcher $fileNodesFetcher; - - private FileTypeMapper $fileTypeMapper; - /** @var array> */ private array $methodNodes = []; public function __construct( - FunctionSignatureMapProvider $functionSignatureMapProvider, - FileNodesFetcher $fileNodesFetcher, - FileTypeMapper $fileTypeMapper + private FunctionSignatureMapProvider $functionSignatureMapProvider, + private FileNodesFetcher $fileNodesFetcher, + private FileTypeMapper $fileTypeMapper, ) { - $this->functionSignatureMapProvider = $functionSignatureMapProvider; - $this->fileNodesFetcher = $fileNodesFetcher; - $this->fileTypeMapper = $fileTypeMapper; } public function hasMethodSignature(string $className, string $methodName, int $variant = 0): bool @@ -61,10 +59,8 @@ public function hasMethodSignature(string $className, string $methodName, int $v } /** - * @param string $className - * @param string $methodName * @return array{ClassMethod, string}|null - * @throws \PHPStan\ShouldNotHappenException + * @throws ShouldNotHappenException */ private function findMethodNode(string $className, string $methodName): ?array { @@ -78,12 +74,12 @@ private function findMethodNode(string $className, string $methodName): ?array $nodes = $this->fileNodesFetcher->fetchNodes($stubFile); $classes = $nodes->getClassNodes(); if (count($classes) !== 1) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Class %s stub not found in %s.', $className, $stubFile)); + throw new ShouldNotHappenException(sprintf('Class %s stub not found in %s.', $className, $stubFile)); } $class = $classes[$lowerClassName]; if (count($class) !== 1) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Class %s stub not found in %s.', $className, $stubFile)); + throw new ShouldNotHappenException(sprintf('Class %s stub not found in %s.', $className, $stubFile)); } foreach ($class[0]->getNode()->stmts as $stmt) { @@ -113,7 +109,7 @@ public function hasFunctionSignature(string $name, int $variant = 0): bool return true; } - public function getMethodSignature(string $className, string $methodName, ?\ReflectionMethod $reflectionMethod, int $variant = 0): FunctionSignature + public function getMethodSignature(string $className, string $methodName, ?ReflectionMethod $reflectionMethod, int $variant = 0): FunctionSignature { $lowerClassName = strtolower($className); if (!array_key_exists($lowerClassName, Php8StubsMap::CLASSES)) { @@ -139,7 +135,7 @@ public function getMethodSignature(string $className, string $methodName, ?\Refl if ($this->functionSignatureMapProvider->hasMethodSignature($className, $methodName)) { return $this->mergeSignatures( $signature, - $this->functionSignatureMapProvider->getMethodSignature($className, $methodName, $reflectionMethod, $variant) + $this->functionSignatureMapProvider->getMethodSignature($className, $methodName, $reflectionMethod, $variant), ); } @@ -165,14 +161,14 @@ public function getFunctionSignature(string $functionName, ?string $className, i $nodes = $this->fileNodesFetcher->fetchNodes($stubFile); $functions = $nodes->getFunctionNodes(); if (count($functions) !== 1) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Function %s stub not found in %s.', $functionName, $stubFile)); + throw new ShouldNotHappenException(sprintf('Function %s stub not found in %s.', $functionName, $stubFile)); } $signature = $this->getSignature($functions[$lowerName]->getNode(), null, $stubFile); if ($this->functionSignatureMapProvider->hasFunctionSignature($functionName)) { return $this->mergeSignatures( $signature, - $this->functionSignatureMapProvider->getFunctionSignature($functionName, $className) + $this->functionSignatureMapProvider->getFunctionSignature($functionName, $className), ); } @@ -197,12 +193,12 @@ private function mergeSignatures(FunctionSignature $nativeSignature, FunctionSig $nativeParameterType, TypehintHelper::decideType( $nativeParameter->getType(), - $functionMapParameter->getType() - ) + $functionMapParameter->getType(), + ), ), $nativeParameterType, $nativeParameter->passedByReference()->yes() ? $functionMapParameter->passedByReference() : $nativeParameter->passedByReference(), - $nativeParameter->isVariadic() + $nativeParameter->isVariadic(), ); } @@ -214,8 +210,8 @@ private function mergeSignatures(FunctionSignature $nativeSignature, FunctionSig $nativeReturnType, TypehintHelper::decideType( $nativeSignature->getReturnType(), - $functionMapSignature->getReturnType() - ) + $functionMapSignature->getReturnType(), + ), ); } @@ -223,7 +219,7 @@ private function mergeSignatures(FunctionSignature $nativeSignature, FunctionSig $parameters, $returnType, $nativeReturnType, - $nativeSignature->isVariadic() + $nativeSignature->isVariadic(), ); } @@ -238,8 +234,6 @@ public function hasFunctionMetadata(string $name): bool } /** - * @param string $className - * @param string $methodName * @return array{hasSideEffects: bool} */ public function getMethodMetadata(string $className, string $methodName): array @@ -248,7 +242,6 @@ public function getMethodMetadata(string $className, string $methodName): array } /** - * @param string $functionName * @return array{hasSideEffects: bool} */ public function getFunctionMetadata(string $functionName): array @@ -258,28 +251,31 @@ public function getFunctionMetadata(string $functionName): array /** * @param ClassMethod|Function_ $function - * @param string $stubFile - * @return FunctionSignature */ private function getSignature( FunctionLike $function, ?string $className, - string $stubFile + string $stubFile, ): FunctionSignature { $phpDocParameterTypes = null; $phpDocReturnType = null; if ($function->getDocComment() !== null) { + if ($function instanceof ClassMethod) { + $functionName = $function->name->toString(); + } elseif ($function->namespacedName !== null) { + $functionName = $function->namespacedName->toString(); + } else { + throw new ShouldNotHappenException(); + } $phpDoc = $this->fileTypeMapper->getResolvedPhpDoc( $stubFile, $className, null, - $function instanceof ClassMethod ? $function->name->toString() : $function->namespacedName->toString(), - $function->getDocComment()->getText() + $functionName, + $function->getDocComment()->getText(), ); - $phpDocParameterTypes = array_map(static function (ParamTag $param): Type { - return $param->getType(); - }, $phpDoc->getParamTags()); + $phpDocParameterTypes = array_map(static fn (ParamTag $param): Type => $param->getType(), $phpDoc->getParamTags()); if ($phpDoc->getReturnTag() !== null) { $phpDocReturnType = $phpDoc->getReturnTag()->getType(); } @@ -289,20 +285,16 @@ private function getSignature( foreach ($function->getParams() as $param) { $name = $param->var; if (!$name instanceof Variable || !is_string($name->name)) { - throw new \PHPStan\ShouldNotHappenException(); - } - if ($name->name === 'array') { - $parameterType = new ArrayType(new MixedType(), new MixedType()); - } else { - $parameterType = ParserNodeTypeToPHPStanType::resolve($param->type, null); + throw new ShouldNotHappenException(); } + $parameterType = ParserNodeTypeToPHPStanType::resolve($param->type, null); $parameters[] = new ParameterSignature( $name->name, $param->default !== null || $param->variadic, TypehintHelper::decideType($parameterType, $phpDocParameterTypes[$name->name] ?? null), $parameterType, $param->byRef ? PassedByReference::createCreatesNewVariable() : PassedByReference::createNo(), - $param->variadic + $param->variadic, ); $variadic = $variadic || $param->variadic; @@ -314,7 +306,7 @@ private function getSignature( $parameters, TypehintHelper::decideType($returnType, $phpDocReturnType ?? null), $returnType, - $variadic + $variadic, ); } diff --git a/src/Reflection/SignatureMap/SignatureMapParser.php b/src/Reflection/SignatureMap/SignatureMapParser.php index 2a4e203d85..68cf3fbdf6 100644 --- a/src/Reflection/SignatureMap/SignatureMapParser.php +++ b/src/Reflection/SignatureMap/SignatureMapParser.php @@ -2,19 +2,24 @@ namespace PHPStan\Reflection\SignatureMap; +use Nette\Utils\Strings; use PHPStan\Analyser\NameScope; use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Reflection\PassedByReference; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\MixedType; use PHPStan\Type\Type; +use function array_slice; +use function strpos; +use function substr; class SignatureMapParser { - private \PHPStan\PhpDoc\TypeStringResolver $typeStringResolver; + private TypeStringResolver $typeStringResolver; public function __construct( - TypeStringResolver $typeNodeResolver + TypeStringResolver $typeNodeResolver, ) { $this->typeStringResolver = $typeNodeResolver; @@ -22,8 +27,6 @@ public function __construct( /** * @param mixed[] $map - * @param string|null $className - * @return \PHPStan\Reflection\SignatureMap\FunctionSignature */ public function getFunctionSignature(array $map, ?string $className): FunctionSignature { @@ -39,7 +42,7 @@ public function getFunctionSignature(array $map, ?string $className): FunctionSi $parameterSignatures, $this->getTypeFromString($map[0], $className), new MixedType(), - $hasVariadic + $hasVariadic, ); } @@ -54,7 +57,7 @@ private function getTypeFromString(string $typeString, ?string $className): Type /** * @param array $parameterMap - * @return array + * @return array */ private function getParameters(array $parameterMap): array { @@ -67,7 +70,7 @@ private function getParameters(array $parameterMap): array $this->getTypeFromString($typeString, null), new MixedType(), $passedByReference, - $isVariadic + $isVariadic, ); } @@ -75,17 +78,16 @@ private function getParameters(array $parameterMap): array } /** - * @param string $parameterNameString * @return mixed[] */ private function getParameterInfoFromName(string $parameterNameString): array { - $matches = \Nette\Utils\Strings::match( + $matches = Strings::match( $parameterNameString, - '#^(?P&(?:\.\.\.)?r?w?_?)?(?P\.\.\.)?(?P[^=]+)?(?P=)?($)#' + '#^(?P&(?:\.\.\.)?r?w?_?)?(?P\.\.\.)?(?P[^=]+)?(?P=)?($)#', ); if ($matches === null || !isset($matches['optional'])) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $isVariadic = $matches['variadic'] !== ''; @@ -97,7 +99,7 @@ private function getParameterInfoFromName(string $parameterNameString): array } if (strpos($reference, '&rw') === 0) { $passedByReference = PassedByReference::createReadsArgument(); - } elseif (strpos($reference, '&w') === 0) { + } elseif (strpos($reference, '&w') === 0 || strpos($reference, '&') === 0) { $passedByReference = PassedByReference::createCreatesNewVariable(); } else { $passedByReference = PassedByReference::createNo(); diff --git a/src/Reflection/SignatureMap/SignatureMapProvider.php b/src/Reflection/SignatureMap/SignatureMapProvider.php index c1f49dbddb..b721ea2b04 100644 --- a/src/Reflection/SignatureMap/SignatureMapProvider.php +++ b/src/Reflection/SignatureMap/SignatureMapProvider.php @@ -2,6 +2,8 @@ namespace PHPStan\Reflection\SignatureMap; +use ReflectionMethod; + interface SignatureMapProvider { @@ -9,7 +11,7 @@ public function hasMethodSignature(string $className, string $methodName, int $v public function hasFunctionSignature(string $name, int $variant = 0): bool; - public function getMethodSignature(string $className, string $methodName, ?\ReflectionMethod $reflectionMethod, int $variant = 0): FunctionSignature; + public function getMethodSignature(string $className, string $methodName, ?ReflectionMethod $reflectionMethod, int $variant = 0): FunctionSignature; public function getFunctionSignature(string $functionName, ?string $className, int $variant = 0): FunctionSignature; @@ -18,14 +20,11 @@ public function hasMethodMetadata(string $className, string $methodName): bool; public function hasFunctionMetadata(string $name): bool; /** - * @param string $className - * @param string $methodName * @return array{hasSideEffects: bool} */ public function getMethodMetadata(string $className, string $methodName): array; /** - * @param string $functionName * @return array{hasSideEffects: bool} */ public function getFunctionMetadata(string $functionName): array; diff --git a/src/Reflection/SignatureMap/SignatureMapProviderFactory.php b/src/Reflection/SignatureMap/SignatureMapProviderFactory.php index 202e1cb71b..2e318eefeb 100644 --- a/src/Reflection/SignatureMap/SignatureMapProviderFactory.php +++ b/src/Reflection/SignatureMap/SignatureMapProviderFactory.php @@ -7,21 +7,12 @@ class SignatureMapProviderFactory { - private PhpVersion $phpVersion; - - private FunctionSignatureMapProvider $functionSignatureMapProvider; - - private Php8SignatureMapProvider $php8SignatureMapProvider; - public function __construct( - PhpVersion $phpVersion, - FunctionSignatureMapProvider $functionSignatureMapProvider, - Php8SignatureMapProvider $php8SignatureMapProvider + private PhpVersion $phpVersion, + private FunctionSignatureMapProvider $functionSignatureMapProvider, + private Php8SignatureMapProvider $php8SignatureMapProvider, ) { - $this->phpVersion = $phpVersion; - $this->functionSignatureMapProvider = $functionSignatureMapProvider; - $this->php8SignatureMapProvider = $php8SignatureMapProvider; } public function create(): SignatureMapProvider diff --git a/src/Reflection/TrivialParametersAcceptor.php b/src/Reflection/TrivialParametersAcceptor.php index c574046278..9c773dfc77 100644 --- a/src/Reflection/TrivialParametersAcceptor.php +++ b/src/Reflection/TrivialParametersAcceptor.php @@ -26,7 +26,7 @@ public function getResolvedTemplateTypeMap(): TemplateTypeMap } /** - * @return array + * @return array */ public function getParameters(): array { diff --git a/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php index 70fae4e624..51e964c685 100644 --- a/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php @@ -11,16 +11,11 @@ use PHPStan\Reflection\Php\DummyParameter; use PHPStan\Reflection\ResolvedMethodReflection; use PHPStan\Type\Type; +use function array_map; class CallbackUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection { - private MethodReflection $methodReflection; - - private ClassReflection $resolvedDeclaringClass; - - private bool $resolveTemplateTypeMapToBounds; - /** @var callable(Type): Type */ private $transformStaticTypeCallback; @@ -29,21 +24,15 @@ class CallbackUnresolvedMethodPrototypeReflection implements UnresolvedMethodPro private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; /** - * @param MethodReflection $methodReflection - * @param ClassReflection $resolvedDeclaringClass - * @param bool $resolveTemplateTypeMapToBounds * @param callable(Type): Type $transformStaticTypeCallback */ public function __construct( - MethodReflection $methodReflection, - ClassReflection $resolvedDeclaringClass, - bool $resolveTemplateTypeMapToBounds, - callable $transformStaticTypeCallback + private MethodReflection $methodReflection, + private ClassReflection $resolvedDeclaringClass, + private bool $resolveTemplateTypeMapToBounds, + callable $transformStaticTypeCallback, ) { - $this->methodReflection = $methodReflection; - $this->resolvedDeclaringClass = $resolvedDeclaringClass; - $this->resolveTemplateTypeMapToBounds = $resolveTemplateTypeMapToBounds; $this->transformStaticTypeCallback = $transformStaticTypeCallback; } @@ -57,7 +46,7 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototype $this->methodReflection, $this->resolvedDeclaringClass, false, - $this->transformStaticTypeCallback + $this->transformStaticTypeCallback, ); } @@ -75,7 +64,7 @@ public function getTransformedMethod(): MethodReflection return $this->transformedMethod = new ResolvedMethodReflection( $this->transformMethodWithStaticType($this->resolvedDeclaringClass, $this->methodReflection), - $this->resolveTemplateTypeMapToBounds ? $templateTypeMap->resolveToBounds() : $templateTypeMap + $this->resolveTemplateTypeMapToBounds ? $templateTypeMap->resolveToBounds() : $templateTypeMap, ); } @@ -85,30 +74,26 @@ public function withCalledOnType(Type $type): UnresolvedMethodPrototypeReflectio $this->methodReflection, $this->resolvedDeclaringClass, $this->resolveTemplateTypeMapToBounds, - $type + $type, ); } private function transformMethodWithStaticType(ClassReflection $declaringClass, MethodReflection $method): MethodReflection { - $variants = array_map(function (ParametersAcceptor $acceptor): ParametersAcceptor { - return new FunctionVariant( - $acceptor->getTemplateTypeMap(), - $acceptor->getResolvedTemplateTypeMap(), - array_map(function (ParameterReflection $parameter): ParameterReflection { - return new DummyParameter( - $parameter->getName(), - $this->transformStaticType($parameter->getType()), - $parameter->isOptional(), - $parameter->passedByReference(), - $parameter->isVariadic(), - $parameter->getDefaultValue() - ); - }, $acceptor->getParameters()), - $acceptor->isVariadic(), - $this->transformStaticType($acceptor->getReturnType()) - ); - }, $method->getVariants()); + $variants = array_map(fn (ParametersAcceptor $acceptor): ParametersAcceptor => new FunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + array_map(fn (ParameterReflection $parameter): ParameterReflection => new DummyParameter( + $parameter->getName(), + $this->transformStaticType($parameter->getType()), + $parameter->isOptional(), + $parameter->passedByReference(), + $parameter->isVariadic(), + $parameter->getDefaultValue(), + ), $acceptor->getParameters()), + $acceptor->isVariadic(), + $this->transformStaticType($acceptor->getReturnType()), + ), $method->getVariants()); return new ChangedTypeMethodReflection($declaringClass, $method, $variants); } diff --git a/src/Reflection/Type/CallbackUnresolvedPropertyPrototypeReflection.php b/src/Reflection/Type/CallbackUnresolvedPropertyPrototypeReflection.php index 903b081c58..ab53a24f99 100644 --- a/src/Reflection/Type/CallbackUnresolvedPropertyPrototypeReflection.php +++ b/src/Reflection/Type/CallbackUnresolvedPropertyPrototypeReflection.php @@ -11,12 +11,6 @@ class CallbackUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection { - private PropertyReflection $propertyReflection; - - private ClassReflection $resolvedDeclaringClass; - - private bool $resolveTemplateTypeMapToBounds; - /** @var callable(Type): Type */ private $transformStaticTypeCallback; @@ -25,21 +19,15 @@ class CallbackUnresolvedPropertyPrototypeReflection implements UnresolvedPropert private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; /** - * @param PropertyReflection $propertyReflection - * @param ClassReflection $resolvedDeclaringClass - * @param bool $resolveTemplateTypeMapToBounds * @param callable(Type): Type $transformStaticTypeCallback */ public function __construct( - PropertyReflection $propertyReflection, - ClassReflection $resolvedDeclaringClass, - bool $resolveTemplateTypeMapToBounds, - callable $transformStaticTypeCallback + private PropertyReflection $propertyReflection, + private ClassReflection $resolvedDeclaringClass, + private bool $resolveTemplateTypeMapToBounds, + callable $transformStaticTypeCallback, ) { - $this->propertyReflection = $propertyReflection; - $this->resolvedDeclaringClass = $resolvedDeclaringClass; - $this->resolveTemplateTypeMapToBounds = $resolveTemplateTypeMapToBounds; $this->transformStaticTypeCallback = $transformStaticTypeCallback; } @@ -53,7 +41,7 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedPropertyPrototy $this->propertyReflection, $this->resolvedDeclaringClass, false, - $this->transformStaticTypeCallback + $this->transformStaticTypeCallback, ); } @@ -71,7 +59,7 @@ public function getTransformedProperty(): PropertyReflection return $this->transformedProperty = new ResolvedPropertyReflection( $this->transformPropertyWithStaticType($this->resolvedDeclaringClass, $this->propertyReflection), - $this->resolveTemplateTypeMapToBounds ? $templateTypeMap->resolveToBounds() : $templateTypeMap + $this->resolveTemplateTypeMapToBounds ? $templateTypeMap->resolveToBounds() : $templateTypeMap, ); } @@ -81,7 +69,7 @@ public function withFechedOnType(Type $type): UnresolvedPropertyPrototypeReflect $this->propertyReflection, $this->resolvedDeclaringClass, $this->resolveTemplateTypeMapToBounds, - $type + $type, ); } diff --git a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php index 069592d59d..dfd47cbef2 100644 --- a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php @@ -13,33 +13,22 @@ use PHPStan\Type\StaticType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; +use function array_map; class CalledOnTypeUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection { - private MethodReflection $methodReflection; - - private ClassReflection $resolvedDeclaringClass; - - private bool $resolveTemplateTypeMapToBounds; - - private Type $calledOnType; - private ?MethodReflection $transformedMethod = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; public function __construct( - MethodReflection $methodReflection, - ClassReflection $resolvedDeclaringClass, - bool $resolveTemplateTypeMapToBounds, - Type $calledOnType + private MethodReflection $methodReflection, + private ClassReflection $resolvedDeclaringClass, + private bool $resolveTemplateTypeMapToBounds, + private Type $calledOnType, ) { - $this->methodReflection = $methodReflection; - $this->resolvedDeclaringClass = $resolvedDeclaringClass; - $this->resolveTemplateTypeMapToBounds = $resolveTemplateTypeMapToBounds; - $this->calledOnType = $calledOnType; } public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototypeReflection @@ -52,7 +41,7 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototype $this->methodReflection, $this->resolvedDeclaringClass, false, - $this->calledOnType + $this->calledOnType, ); } @@ -70,7 +59,7 @@ public function getTransformedMethod(): MethodReflection return $this->transformedMethod = new ResolvedMethodReflection( $this->transformMethodWithStaticType($this->resolvedDeclaringClass, $this->methodReflection), - $this->resolveTemplateTypeMapToBounds ? $templateTypeMap->resolveToBounds() : $templateTypeMap + $this->resolveTemplateTypeMapToBounds ? $templateTypeMap->resolveToBounds() : $templateTypeMap, ); } @@ -80,30 +69,26 @@ public function withCalledOnType(Type $type): UnresolvedMethodPrototypeReflectio $this->methodReflection, $this->resolvedDeclaringClass, $this->resolveTemplateTypeMapToBounds, - $type + $type, ); } private function transformMethodWithStaticType(ClassReflection $declaringClass, MethodReflection $method): MethodReflection { - $variants = array_map(function (ParametersAcceptor $acceptor): ParametersAcceptor { - return new FunctionVariant( - $acceptor->getTemplateTypeMap(), - $acceptor->getResolvedTemplateTypeMap(), - array_map(function (ParameterReflection $parameter): ParameterReflection { - return new DummyParameter( - $parameter->getName(), - $this->transformStaticType($parameter->getType()), - $parameter->isOptional(), - $parameter->passedByReference(), - $parameter->isVariadic(), - $parameter->getDefaultValue() - ); - }, $acceptor->getParameters()), - $acceptor->isVariadic(), - $this->transformStaticType($acceptor->getReturnType()) - ); - }, $method->getVariants()); + $variants = array_map(fn (ParametersAcceptor $acceptor): ParametersAcceptor => new FunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + array_map(fn (ParameterReflection $parameter): ParameterReflection => new DummyParameter( + $parameter->getName(), + $this->transformStaticType($parameter->getType()), + $parameter->isOptional(), + $parameter->passedByReference(), + $parameter->isVariadic(), + $parameter->getDefaultValue(), + ), $acceptor->getParameters()), + $acceptor->isVariadic(), + $this->transformStaticType($acceptor->getReturnType()), + ), $method->getVariants()); return new ChangedTypeMethodReflection($declaringClass, $method, $variants); } diff --git a/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php b/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php index 59bd33d067..13e7f5b875 100644 --- a/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php +++ b/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php @@ -13,29 +13,17 @@ class CalledOnTypeUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection { - private PropertyReflection $propertyReflection; - - private ClassReflection $resolvedDeclaringClass; - - private bool $resolveTemplateTypeMapToBounds; - - private Type $fetchedOnType; - private ?PropertyReflection $transformedProperty = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; public function __construct( - PropertyReflection $propertyReflection, - ClassReflection $resolvedDeclaringClass, - bool $resolveTemplateTypeMapToBounds, - Type $fetchedOnType + private PropertyReflection $propertyReflection, + private ClassReflection $resolvedDeclaringClass, + private bool $resolveTemplateTypeMapToBounds, + private Type $fetchedOnType, ) { - $this->propertyReflection = $propertyReflection; - $this->resolvedDeclaringClass = $resolvedDeclaringClass; - $this->resolveTemplateTypeMapToBounds = $resolveTemplateTypeMapToBounds; - $this->fetchedOnType = $fetchedOnType; } public function doNotResolveTemplateTypeMapToBounds(): UnresolvedPropertyPrototypeReflection @@ -48,7 +36,7 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedPropertyPrototy $this->propertyReflection, $this->resolvedDeclaringClass, false, - $this->fetchedOnType + $this->fetchedOnType, ); } @@ -66,7 +54,7 @@ public function getTransformedProperty(): PropertyReflection return $this->transformedProperty = new ResolvedPropertyReflection( $this->transformPropertyWithStaticType($this->resolvedDeclaringClass, $this->propertyReflection), - $this->resolveTemplateTypeMapToBounds ? $templateTypeMap->resolveToBounds() : $templateTypeMap + $this->resolveTemplateTypeMapToBounds ? $templateTypeMap->resolveToBounds() : $templateTypeMap, ); } @@ -76,7 +64,7 @@ public function withFechedOnType(Type $type): UnresolvedPropertyPrototypeReflect $this->propertyReflection, $this->resolvedDeclaringClass, $this->resolveTemplateTypeMapToBounds, - $type + $type, ); } diff --git a/src/Reflection/Type/IntersectionTypeMethodReflection.php b/src/Reflection/Type/IntersectionTypeMethodReflection.php index ad136f3abf..8c547ca3d0 100644 --- a/src/Reflection/Type/IntersectionTypeMethodReflection.php +++ b/src/Reflection/Type/IntersectionTypeMethodReflection.php @@ -10,23 +10,18 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function array_map; +use function count; +use function implode; class IntersectionTypeMethodReflection implements MethodReflection { - private string $methodName; - - /** @var MethodReflection[] */ - private array $methods; - /** - * @param string $methodName - * @param \PHPStan\Reflection\MethodReflection[] $methods + * @param MethodReflection[] $methods */ - public function __construct(string $methodName, array $methods) + public function __construct(private string $methodName, private array $methods) { - $this->methodName = $methodName; - $this->methods = $methods; } public function getDeclaringClass(): ClassReflection @@ -79,28 +74,20 @@ public function getPrototype(): ClassMemberReflection public function getVariants(): array { - $returnType = TypeCombinator::intersect(...array_map(static function (MethodReflection $method): Type { - return TypeCombinator::intersect(...array_map(static function (ParametersAcceptor $acceptor): Type { - return $acceptor->getReturnType(); - }, $method->getVariants())); - }, $this->methods)); - - return array_map(static function (ParametersAcceptor $acceptor) use ($returnType): ParametersAcceptor { - return new FunctionVariant( - $acceptor->getTemplateTypeMap(), - $acceptor->getResolvedTemplateTypeMap(), - $acceptor->getParameters(), - $acceptor->isVariadic(), - $returnType - ); - }, $this->methods[0]->getVariants()); + $returnType = TypeCombinator::intersect(...array_map(static fn (MethodReflection $method): Type => TypeCombinator::intersect(...array_map(static fn (ParametersAcceptor $acceptor): Type => $acceptor->getReturnType(), $method->getVariants())), $this->methods)); + + return array_map(static fn (ParametersAcceptor $acceptor): ParametersAcceptor => new FunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + $acceptor->getParameters(), + $acceptor->isVariadic(), + $returnType, + ), $this->methods[0]->getVariants()); } public function isDeprecated(): TrinaryLogic { - return TrinaryLogic::maxMin(...array_map(static function (MethodReflection $method): TrinaryLogic { - return $method->isDeprecated(); - }, $this->methods)); + return TrinaryLogic::maxMin(...array_map(static fn (MethodReflection $method): TrinaryLogic => $method->isDeprecated(), $this->methods)); } public function getDeprecatedDescription(): ?string @@ -127,16 +114,12 @@ public function getDeprecatedDescription(): ?string public function isFinal(): TrinaryLogic { - return TrinaryLogic::maxMin(...array_map(static function (MethodReflection $method): TrinaryLogic { - return $method->isFinal(); - }, $this->methods)); + return TrinaryLogic::maxMin(...array_map(static fn (MethodReflection $method): TrinaryLogic => $method->isFinal(), $this->methods)); } public function isInternal(): TrinaryLogic { - return TrinaryLogic::maxMin(...array_map(static function (MethodReflection $method): TrinaryLogic { - return $method->isInternal(); - }, $this->methods)); + return TrinaryLogic::maxMin(...array_map(static fn (MethodReflection $method): TrinaryLogic => $method->isInternal(), $this->methods)); } public function getThrowType(): ?Type @@ -161,9 +144,7 @@ public function getThrowType(): ?Type public function hasSideEffects(): TrinaryLogic { - return TrinaryLogic::maxMin(...array_map(static function (MethodReflection $method): TrinaryLogic { - return $method->hasSideEffects(); - }, $this->methods)); + return TrinaryLogic::maxMin(...array_map(static fn (MethodReflection $method): TrinaryLogic => $method->hasSideEffects(), $this->methods)); } public function getDocComment(): ?string diff --git a/src/Reflection/Type/IntersectionTypePropertyReflection.php b/src/Reflection/Type/IntersectionTypePropertyReflection.php index 9ecb2f7947..68db339d3c 100644 --- a/src/Reflection/Type/IntersectionTypePropertyReflection.php +++ b/src/Reflection/Type/IntersectionTypePropertyReflection.php @@ -7,19 +7,18 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function array_map; +use function count; +use function implode; class IntersectionTypePropertyReflection implements PropertyReflection { - /** @var PropertyReflection[] */ - private array $properties; - /** - * @param \PHPStan\Reflection\PropertyReflection[] $properties + * @param PropertyReflection[] $properties */ - public function __construct(array $properties) + public function __construct(private array $properties) { - $this->properties = $properties; } public function getDeclaringClass(): ClassReflection @@ -62,9 +61,7 @@ public function isPublic(): bool public function isDeprecated(): TrinaryLogic { - return TrinaryLogic::maxMin(...array_map(static function (PropertyReflection $property): TrinaryLogic { - return $property->isDeprecated(); - }, $this->properties)); + return TrinaryLogic::maxMin(...array_map(static fn (PropertyReflection $property): TrinaryLogic => $property->isDeprecated(), $this->properties)); } public function getDeprecatedDescription(): ?string @@ -91,9 +88,7 @@ public function getDeprecatedDescription(): ?string public function isInternal(): TrinaryLogic { - return TrinaryLogic::maxMin(...array_map(static function (PropertyReflection $property): TrinaryLogic { - return $property->isInternal(); - }, $this->properties)); + return TrinaryLogic::maxMin(...array_map(static fn (PropertyReflection $property): TrinaryLogic => $property->isInternal(), $this->properties)); } public function getDocComment(): ?string @@ -103,16 +98,12 @@ public function getDocComment(): ?string public function getReadableType(): Type { - return TypeCombinator::intersect(...array_map(static function (PropertyReflection $property): Type { - return $property->getReadableType(); - }, $this->properties)); + return TypeCombinator::intersect(...array_map(static fn (PropertyReflection $property): Type => $property->getReadableType(), $this->properties)); } public function getWritableType(): Type { - return TypeCombinator::intersect(...array_map(static function (PropertyReflection $property): Type { - return $property->getWritableType(); - }, $this->properties)); + return TypeCombinator::intersect(...array_map(static fn (PropertyReflection $property): Type => $property->getWritableType(), $this->properties)); } public function canChangeTypeAfterAssignment(): bool diff --git a/src/Reflection/Type/IntersectionTypeUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/IntersectionTypeUnresolvedMethodPrototypeReflection.php index d8252da113..be6d2d944f 100644 --- a/src/Reflection/Type/IntersectionTypeUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/IntersectionTypeUnresolvedMethodPrototypeReflection.php @@ -4,15 +4,11 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Type\Type; +use function array_map; class IntersectionTypeUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection { - private string $methodName; - - /** @var UnresolvedMethodPrototypeReflection[] */ - private array $methodPrototypes; - private ?MethodReflection $transformedMethod = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; @@ -21,12 +17,10 @@ class IntersectionTypeUnresolvedMethodPrototypeReflection implements UnresolvedM * @param UnresolvedMethodPrototypeReflection[] $methodPrototypes */ public function __construct( - string $methodName, - array $methodPrototypes + private string $methodName, + private array $methodPrototypes, ) { - $this->methodName = $methodName; - $this->methodPrototypes = $methodPrototypes; } public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototypeReflection @@ -35,9 +29,7 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototype return $this->cachedDoNotResolveTemplateTypeMapToBounds; } - return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->methodName, array_map(static function (UnresolvedMethodPrototypeReflection $prototype): UnresolvedMethodPrototypeReflection { - return $prototype->doNotResolveTemplateTypeMapToBounds(); - }, $this->methodPrototypes)); + return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->methodName, array_map(static fn (UnresolvedMethodPrototypeReflection $prototype): UnresolvedMethodPrototypeReflection => $prototype->doNotResolveTemplateTypeMapToBounds(), $this->methodPrototypes)); } public function getNakedMethod(): MethodReflection @@ -50,18 +42,14 @@ public function getTransformedMethod(): MethodReflection if ($this->transformedMethod !== null) { return $this->transformedMethod; } - $methods = array_map(static function (UnresolvedMethodPrototypeReflection $prototype): MethodReflection { - return $prototype->getTransformedMethod(); - }, $this->methodPrototypes); + $methods = array_map(static fn (UnresolvedMethodPrototypeReflection $prototype): MethodReflection => $prototype->getTransformedMethod(), $this->methodPrototypes); return $this->transformedMethod = new IntersectionTypeMethodReflection($this->methodName, $methods); } public function withCalledOnType(Type $type): UnresolvedMethodPrototypeReflection { - return new self($this->methodName, array_map(static function (UnresolvedMethodPrototypeReflection $prototype) use ($type): UnresolvedMethodPrototypeReflection { - return $prototype->withCalledOnType($type); - }, $this->methodPrototypes)); + return new self($this->methodName, array_map(static fn (UnresolvedMethodPrototypeReflection $prototype): UnresolvedMethodPrototypeReflection => $prototype->withCalledOnType($type), $this->methodPrototypes)); } } diff --git a/src/Reflection/Type/IntersectionTypeUnresolvedPropertyPrototypeReflection.php b/src/Reflection/Type/IntersectionTypeUnresolvedPropertyPrototypeReflection.php index 4be1d50ea0..fdfea4ebf9 100644 --- a/src/Reflection/Type/IntersectionTypeUnresolvedPropertyPrototypeReflection.php +++ b/src/Reflection/Type/IntersectionTypeUnresolvedPropertyPrototypeReflection.php @@ -4,15 +4,11 @@ use PHPStan\Reflection\PropertyReflection; use PHPStan\Type\Type; +use function array_map; class IntersectionTypeUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection { - private string $propertyName; - - /** @var UnresolvedPropertyPrototypeReflection[] */ - private array $propertyPrototypes; - private ?PropertyReflection $transformedProperty = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; @@ -21,12 +17,10 @@ class IntersectionTypeUnresolvedPropertyPrototypeReflection implements Unresolve * @param UnresolvedPropertyPrototypeReflection[] $propertyPrototypes */ public function __construct( - string $propertyName, - array $propertyPrototypes + private string $propertyName, + private array $propertyPrototypes, ) { - $this->propertyName = $propertyName; - $this->propertyPrototypes = $propertyPrototypes; } public function doNotResolveTemplateTypeMapToBounds(): UnresolvedPropertyPrototypeReflection @@ -35,9 +29,7 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedPropertyPrototy return $this->cachedDoNotResolveTemplateTypeMapToBounds; } - return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->propertyName, array_map(static function (UnresolvedPropertyPrototypeReflection $prototype): UnresolvedPropertyPrototypeReflection { - return $prototype->doNotResolveTemplateTypeMapToBounds(); - }, $this->propertyPrototypes)); + return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->propertyName, array_map(static fn (UnresolvedPropertyPrototypeReflection $prototype): UnresolvedPropertyPrototypeReflection => $prototype->doNotResolveTemplateTypeMapToBounds(), $this->propertyPrototypes)); } public function getNakedProperty(): PropertyReflection @@ -50,18 +42,14 @@ public function getTransformedProperty(): PropertyReflection if ($this->transformedProperty !== null) { return $this->transformedProperty; } - $properties = array_map(static function (UnresolvedPropertyPrototypeReflection $prototype): PropertyReflection { - return $prototype->getTransformedProperty(); - }, $this->propertyPrototypes); + $properties = array_map(static fn (UnresolvedPropertyPrototypeReflection $prototype): PropertyReflection => $prototype->getTransformedProperty(), $this->propertyPrototypes); return $this->transformedProperty = new IntersectionTypePropertyReflection($properties); } public function withFechedOnType(Type $type): UnresolvedPropertyPrototypeReflection { - return new self($this->propertyName, array_map(static function (UnresolvedPropertyPrototypeReflection $prototype) use ($type): UnresolvedPropertyPrototypeReflection { - return $prototype->withFechedOnType($type); - }, $this->propertyPrototypes)); + return new self($this->propertyName, array_map(static fn (UnresolvedPropertyPrototypeReflection $prototype): UnresolvedPropertyPrototypeReflection => $prototype->withFechedOnType($type), $this->propertyPrototypes)); } } diff --git a/src/Reflection/Type/UnionTypeMethodReflection.php b/src/Reflection/Type/UnionTypeMethodReflection.php index 54ca673a7a..cde9bb3f69 100644 --- a/src/Reflection/Type/UnionTypeMethodReflection.php +++ b/src/Reflection/Type/UnionTypeMethodReflection.php @@ -10,23 +10,18 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function array_map; +use function count; +use function implode; class UnionTypeMethodReflection implements MethodReflection { - private string $methodName; - - /** @var MethodReflection[] */ - private array $methods; - /** - * @param string $methodName - * @param \PHPStan\Reflection\MethodReflection[] $methods + * @param MethodReflection[] $methods */ - public function __construct(string $methodName, array $methods) + public function __construct(private string $methodName, private array $methods) { - $this->methodName = $methodName; - $this->methods = $methods; } public function getDeclaringClass(): ClassReflection @@ -80,28 +75,20 @@ public function getPrototype(): ClassMemberReflection public function getVariants(): array { $variants = $this->methods[0]->getVariants(); - $returnType = TypeCombinator::union(...array_map(static function (MethodReflection $method): Type { - return TypeCombinator::union(...array_map(static function (ParametersAcceptor $acceptor): Type { - return $acceptor->getReturnType(); - }, $method->getVariants())); - }, $this->methods)); - - return array_map(static function (ParametersAcceptor $acceptor) use ($returnType): ParametersAcceptor { - return new FunctionVariant( - $acceptor->getTemplateTypeMap(), - $acceptor->getResolvedTemplateTypeMap(), - $acceptor->getParameters(), - $acceptor->isVariadic(), - $returnType - ); - }, $variants); + $returnType = TypeCombinator::union(...array_map(static fn (MethodReflection $method): Type => TypeCombinator::union(...array_map(static fn (ParametersAcceptor $acceptor): Type => $acceptor->getReturnType(), $method->getVariants())), $this->methods)); + + return array_map(static fn (ParametersAcceptor $acceptor): ParametersAcceptor => new FunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + $acceptor->getParameters(), + $acceptor->isVariadic(), + $returnType, + ), $variants); } public function isDeprecated(): TrinaryLogic { - return TrinaryLogic::extremeIdentity(...array_map(static function (MethodReflection $method): TrinaryLogic { - return $method->isDeprecated(); - }, $this->methods)); + return TrinaryLogic::extremeIdentity(...array_map(static fn (MethodReflection $method): TrinaryLogic => $method->isDeprecated(), $this->methods)); } public function getDeprecatedDescription(): ?string @@ -128,16 +115,12 @@ public function getDeprecatedDescription(): ?string public function isFinal(): TrinaryLogic { - return TrinaryLogic::extremeIdentity(...array_map(static function (MethodReflection $method): TrinaryLogic { - return $method->isFinal(); - }, $this->methods)); + return TrinaryLogic::extremeIdentity(...array_map(static fn (MethodReflection $method): TrinaryLogic => $method->isFinal(), $this->methods)); } public function isInternal(): TrinaryLogic { - return TrinaryLogic::extremeIdentity(...array_map(static function (MethodReflection $method): TrinaryLogic { - return $method->isInternal(); - }, $this->methods)); + return TrinaryLogic::extremeIdentity(...array_map(static fn (MethodReflection $method): TrinaryLogic => $method->isInternal(), $this->methods)); } public function getThrowType(): ?Type @@ -162,9 +145,7 @@ public function getThrowType(): ?Type public function hasSideEffects(): TrinaryLogic { - return TrinaryLogic::extremeIdentity(...array_map(static function (MethodReflection $method): TrinaryLogic { - return $method->hasSideEffects(); - }, $this->methods)); + return TrinaryLogic::extremeIdentity(...array_map(static fn (MethodReflection $method): TrinaryLogic => $method->hasSideEffects(), $this->methods)); } public function getDocComment(): ?string diff --git a/src/Reflection/Type/UnionTypePropertyReflection.php b/src/Reflection/Type/UnionTypePropertyReflection.php index 0936fff8d8..d14cfa7b7a 100644 --- a/src/Reflection/Type/UnionTypePropertyReflection.php +++ b/src/Reflection/Type/UnionTypePropertyReflection.php @@ -7,19 +7,18 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function array_map; +use function count; +use function implode; class UnionTypePropertyReflection implements PropertyReflection { - /** @var PropertyReflection[] */ - private array $properties; - /** - * @param \PHPStan\Reflection\PropertyReflection[] $properties + * @param PropertyReflection[] $properties */ - public function __construct(array $properties) + public function __construct(private array $properties) { - $this->properties = $properties; } public function getDeclaringClass(): ClassReflection @@ -62,9 +61,7 @@ public function isPublic(): bool public function isDeprecated(): TrinaryLogic { - return TrinaryLogic::extremeIdentity(...array_map(static function (PropertyReflection $property): TrinaryLogic { - return $property->isDeprecated(); - }, $this->properties)); + return TrinaryLogic::extremeIdentity(...array_map(static fn (PropertyReflection $property): TrinaryLogic => $property->isDeprecated(), $this->properties)); } public function getDeprecatedDescription(): ?string @@ -91,9 +88,7 @@ public function getDeprecatedDescription(): ?string public function isInternal(): TrinaryLogic { - return TrinaryLogic::extremeIdentity(...array_map(static function (PropertyReflection $property): TrinaryLogic { - return $property->isInternal(); - }, $this->properties)); + return TrinaryLogic::extremeIdentity(...array_map(static fn (PropertyReflection $property): TrinaryLogic => $property->isInternal(), $this->properties)); } public function getDocComment(): ?string @@ -103,16 +98,12 @@ public function getDocComment(): ?string public function getReadableType(): Type { - return TypeCombinator::union(...array_map(static function (PropertyReflection $property): Type { - return $property->getReadableType(); - }, $this->properties)); + return TypeCombinator::union(...array_map(static fn (PropertyReflection $property): Type => $property->getReadableType(), $this->properties)); } public function getWritableType(): Type { - return TypeCombinator::union(...array_map(static function (PropertyReflection $property): Type { - return $property->getWritableType(); - }, $this->properties)); + return TypeCombinator::union(...array_map(static fn (PropertyReflection $property): Type => $property->getWritableType(), $this->properties)); } public function canChangeTypeAfterAssignment(): bool diff --git a/src/Reflection/Type/UnionTypeUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/UnionTypeUnresolvedMethodPrototypeReflection.php index 3d8e295dc7..4d8cf48a19 100644 --- a/src/Reflection/Type/UnionTypeUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/UnionTypeUnresolvedMethodPrototypeReflection.php @@ -4,15 +4,11 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Type\Type; +use function array_map; class UnionTypeUnresolvedMethodPrototypeReflection implements UnresolvedMethodPrototypeReflection { - private string $methodName; - - /** @var UnresolvedMethodPrototypeReflection[] */ - private array $methodPrototypes; - private ?MethodReflection $transformedMethod = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; @@ -21,12 +17,10 @@ class UnionTypeUnresolvedMethodPrototypeReflection implements UnresolvedMethodPr * @param UnresolvedMethodPrototypeReflection[] $methodPrototypes */ public function __construct( - string $methodName, - array $methodPrototypes + private string $methodName, + private array $methodPrototypes, ) { - $this->methodName = $methodName; - $this->methodPrototypes = $methodPrototypes; } public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototypeReflection @@ -35,9 +29,7 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedMethodPrototype return $this->cachedDoNotResolveTemplateTypeMapToBounds; } - return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->methodName, array_map(static function (UnresolvedMethodPrototypeReflection $prototype): UnresolvedMethodPrototypeReflection { - return $prototype->doNotResolveTemplateTypeMapToBounds(); - }, $this->methodPrototypes)); + return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->methodName, array_map(static fn (UnresolvedMethodPrototypeReflection $prototype): UnresolvedMethodPrototypeReflection => $prototype->doNotResolveTemplateTypeMapToBounds(), $this->methodPrototypes)); } public function getNakedMethod(): MethodReflection @@ -51,18 +43,14 @@ public function getTransformedMethod(): MethodReflection return $this->transformedMethod; } - $methods = array_map(static function (UnresolvedMethodPrototypeReflection $prototype): MethodReflection { - return $prototype->getTransformedMethod(); - }, $this->methodPrototypes); + $methods = array_map(static fn (UnresolvedMethodPrototypeReflection $prototype): MethodReflection => $prototype->getTransformedMethod(), $this->methodPrototypes); return $this->transformedMethod = new UnionTypeMethodReflection($this->methodName, $methods); } public function withCalledOnType(Type $type): UnresolvedMethodPrototypeReflection { - return new self($this->methodName, array_map(static function (UnresolvedMethodPrototypeReflection $prototype) use ($type): UnresolvedMethodPrototypeReflection { - return $prototype->withCalledOnType($type); - }, $this->methodPrototypes)); + return new self($this->methodName, array_map(static fn (UnresolvedMethodPrototypeReflection $prototype): UnresolvedMethodPrototypeReflection => $prototype->withCalledOnType($type), $this->methodPrototypes)); } } diff --git a/src/Reflection/Type/UnionTypeUnresolvedPropertyPrototypeReflection.php b/src/Reflection/Type/UnionTypeUnresolvedPropertyPrototypeReflection.php index ce86e932b2..746992aadc 100644 --- a/src/Reflection/Type/UnionTypeUnresolvedPropertyPrototypeReflection.php +++ b/src/Reflection/Type/UnionTypeUnresolvedPropertyPrototypeReflection.php @@ -4,15 +4,13 @@ use PHPStan\Reflection\PropertyReflection; use PHPStan\Type\Type; +use function array_map; class UnionTypeUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection { private string $propertyName; - /** @var UnresolvedPropertyPrototypeReflection[] */ - private array $propertyPrototypes; - private ?PropertyReflection $transformedProperty = null; private ?self $cachedDoNotResolveTemplateTypeMapToBounds = null; @@ -22,11 +20,10 @@ class UnionTypeUnresolvedPropertyPrototypeReflection implements UnresolvedProper */ public function __construct( string $methodName, - array $propertyPrototypes + private array $propertyPrototypes, ) { $this->propertyName = $methodName; - $this->propertyPrototypes = $propertyPrototypes; } public function doNotResolveTemplateTypeMapToBounds(): UnresolvedPropertyPrototypeReflection @@ -34,9 +31,7 @@ public function doNotResolveTemplateTypeMapToBounds(): UnresolvedPropertyPrototy if ($this->cachedDoNotResolveTemplateTypeMapToBounds !== null) { return $this->cachedDoNotResolveTemplateTypeMapToBounds; } - return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->propertyName, array_map(static function (UnresolvedPropertyPrototypeReflection $prototype): UnresolvedPropertyPrototypeReflection { - return $prototype->doNotResolveTemplateTypeMapToBounds(); - }, $this->propertyPrototypes)); + return $this->cachedDoNotResolveTemplateTypeMapToBounds = new self($this->propertyName, array_map(static fn (UnresolvedPropertyPrototypeReflection $prototype): UnresolvedPropertyPrototypeReflection => $prototype->doNotResolveTemplateTypeMapToBounds(), $this->propertyPrototypes)); } public function getNakedProperty(): PropertyReflection @@ -50,18 +45,14 @@ public function getTransformedProperty(): PropertyReflection return $this->transformedProperty; } - $methods = array_map(static function (UnresolvedPropertyPrototypeReflection $prototype): PropertyReflection { - return $prototype->getTransformedProperty(); - }, $this->propertyPrototypes); + $methods = array_map(static fn (UnresolvedPropertyPrototypeReflection $prototype): PropertyReflection => $prototype->getTransformedProperty(), $this->propertyPrototypes); return $this->transformedProperty = new UnionTypePropertyReflection($methods); } public function withFechedOnType(Type $type): UnresolvedPropertyPrototypeReflection { - return new self($this->propertyName, array_map(static function (UnresolvedPropertyPrototypeReflection $prototype) use ($type): UnresolvedPropertyPrototypeReflection { - return $prototype->withFechedOnType($type); - }, $this->propertyPrototypes)); + return new self($this->propertyName, array_map(static fn (UnresolvedPropertyPrototypeReflection $prototype): UnresolvedPropertyPrototypeReflection => $prototype->withFechedOnType($type), $this->propertyPrototypes)); } } diff --git a/src/Rules/Api/ApiClassExtendsRule.php b/src/Rules/Api/ApiClassExtendsRule.php index 9ba41ab721..dd90c44f5c 100644 --- a/src/Rules/Api/ApiClassExtendsRule.php +++ b/src/Rules/Api/ApiClassExtendsRule.php @@ -9,6 +9,8 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function count; +use function sprintf; /** * @implements Rule @@ -16,17 +18,11 @@ class ApiClassExtendsRule implements Rule { - private ApiRuleHelper $apiRuleHelper; - - private ReflectionProvider $reflectionProvider; - public function __construct( - ApiRuleHelper $apiRuleHelper, - ReflectionProvider $reflectionProvider + private ApiRuleHelper $apiRuleHelper, + private ReflectionProvider $reflectionProvider, ) { - $this->apiRuleHelper = $apiRuleHelper; - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -56,10 +52,10 @@ public function processNode(Node $node, Scope $scope): array $ruleError = RuleErrorBuilder::message(sprintf( 'Extending %s is not covered by backward compatibility promise. The class might change in a minor PHPStan version.', - $extendedClassReflection->getDisplayName() + $extendedClassReflection->getDisplayName(), ))->tip(sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", - 'https://github.com/phpstan/phpstan/discussions' + 'https://github.com/phpstan/phpstan/discussions', ))->build(); $docBlock = $extendedClassReflection->getResolvedPhpDoc(); diff --git a/src/Rules/Api/ApiClassImplementsRule.php b/src/Rules/Api/ApiClassImplementsRule.php index c0d61f1fc6..2054fa1cd4 100644 --- a/src/Rules/Api/ApiClassImplementsRule.php +++ b/src/Rules/Api/ApiClassImplementsRule.php @@ -10,6 +10,9 @@ use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Type; +use function array_merge; +use function count; +use function sprintf; /** * @implements Rule @@ -17,17 +20,11 @@ class ApiClassImplementsRule implements Rule { - private ApiRuleHelper $apiRuleHelper; - - private ReflectionProvider $reflectionProvider; - public function __construct( - ApiRuleHelper $apiRuleHelper, - ReflectionProvider $reflectionProvider + private ApiRuleHelper $apiRuleHelper, + private ReflectionProvider $reflectionProvider, ) { - $this->apiRuleHelper = $apiRuleHelper; - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -46,7 +43,6 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param Node\Name $name * @return RuleError[] */ private function checkName(Scope $scope, Node\Name $name): array @@ -63,10 +59,10 @@ private function checkName(Scope $scope, Node\Name $name): array $ruleError = RuleErrorBuilder::message(sprintf( 'Implementing %s is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - $implementedClassReflection->getDisplayName() + $implementedClassReflection->getDisplayName(), ))->tip(sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", - 'https://github.com/phpstan/phpstan/discussions' + 'https://github.com/phpstan/phpstan/discussions', ))->build(); if ($implementedClassReflection->getName() === Type::class) { diff --git a/src/Rules/Api/ApiInstantiationRule.php b/src/Rules/Api/ApiInstantiationRule.php index 136c3b6cff..f98f17af0d 100644 --- a/src/Rules/Api/ApiInstantiationRule.php +++ b/src/Rules/Api/ApiInstantiationRule.php @@ -7,6 +7,8 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function sprintf; +use function strpos; /** * @implements Rule @@ -14,17 +16,11 @@ class ApiInstantiationRule implements Rule { - private ApiRuleHelper $apiRuleHelper; - - private ReflectionProvider $reflectionProvider; - public function __construct( - ApiRuleHelper $apiRuleHelper, - ReflectionProvider $reflectionProvider + private ApiRuleHelper $apiRuleHelper, + private ReflectionProvider $reflectionProvider, ) { - $this->apiRuleHelper = $apiRuleHelper; - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -50,10 +46,10 @@ public function processNode(Node $node, Scope $scope): array $ruleError = RuleErrorBuilder::message(sprintf( 'Creating new %s is not covered by backward compatibility promise. The class might change in a minor PHPStan version.', - $classReflection->getDisplayName() + $classReflection->getDisplayName(), ))->tip(sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", - 'https://github.com/phpstan/phpstan/discussions' + 'https://github.com/phpstan/phpstan/discussions', ))->build(); if (!$classReflection->hasConstructor()) { diff --git a/src/Rules/Api/ApiInterfaceExtendsRule.php b/src/Rules/Api/ApiInterfaceExtendsRule.php index 326426b818..94667cad97 100644 --- a/src/Rules/Api/ApiInterfaceExtendsRule.php +++ b/src/Rules/Api/ApiInterfaceExtendsRule.php @@ -10,6 +10,9 @@ use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Type; +use function array_merge; +use function count; +use function sprintf; /** * @implements Rule @@ -17,17 +20,11 @@ class ApiInterfaceExtendsRule implements Rule { - private ApiRuleHelper $apiRuleHelper; - - private ReflectionProvider $reflectionProvider; - public function __construct( - ApiRuleHelper $apiRuleHelper, - ReflectionProvider $reflectionProvider + private ApiRuleHelper $apiRuleHelper, + private ReflectionProvider $reflectionProvider, ) { - $this->apiRuleHelper = $apiRuleHelper; - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -46,8 +43,6 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param Scope $scope - * @param Node\Name $name * @return RuleError[] */ private function checkName(Scope $scope, Node\Name $name): array @@ -64,10 +59,10 @@ private function checkName(Scope $scope, Node\Name $name): array $ruleError = RuleErrorBuilder::message(sprintf( 'Extending %s is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - $extendedInterfaceReflection->getDisplayName() + $extendedInterfaceReflection->getDisplayName(), ))->tip(sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", - 'https://github.com/phpstan/phpstan/discussions' + 'https://github.com/phpstan/phpstan/discussions', ))->build(); if ($extendedInterfaceReflection->getName() === Type::class) { diff --git a/src/Rules/Api/ApiMethodCallRule.php b/src/Rules/Api/ApiMethodCallRule.php index 53db70e53d..0b61144103 100644 --- a/src/Rules/Api/ApiMethodCallRule.php +++ b/src/Rules/Api/ApiMethodCallRule.php @@ -7,6 +7,9 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function count; +use function sprintf; +use function strpos; /** * @implements Rule @@ -14,11 +17,8 @@ class ApiMethodCallRule implements Rule { - private ApiRuleHelper $apiRuleHelper; - - public function __construct(ApiRuleHelper $apiRuleHelper) + public function __construct(private ApiRuleHelper $apiRuleHelper) { - $this->apiRuleHelper = $apiRuleHelper; } public function getNodeType(): string @@ -49,10 +49,10 @@ public function processNode(Node $node, Scope $scope): array $ruleError = RuleErrorBuilder::message(sprintf( 'Calling %s::%s() is not covered by backward compatibility promise. The method might change in a minor PHPStan version.', $declaringClass->getDisplayName(), - $methodReflection->getName() + $methodReflection->getName(), ))->tip(sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", - 'https://github.com/phpstan/phpstan/discussions' + 'https://github.com/phpstan/phpstan/discussions', ))->build(); return [$ruleError]; diff --git a/src/Rules/Api/ApiRuleHelper.php b/src/Rules/Api/ApiRuleHelper.php index 50254219aa..59123647d8 100644 --- a/src/Rules/Api/ApiRuleHelper.php +++ b/src/Rules/Api/ApiRuleHelper.php @@ -4,6 +4,11 @@ use PHPStan\Analyser\Scope; use PHPStan\File\ParentDirectoryRelativePathHelper; +use function dirname; +use function pathinfo; +use function stripos; +use function strpos; +use function strtolower; use const PATHINFO_BASENAME; class ApiRuleHelper @@ -41,7 +46,6 @@ public function isPhpStanCode(Scope $scope, string $namespace, ?string $declarin } /** - * @param string $currentDirectory * @param string[] $parts * @return string[] */ diff --git a/src/Rules/Api/ApiStaticCallRule.php b/src/Rules/Api/ApiStaticCallRule.php index fff1910264..b4ee6f08ca 100644 --- a/src/Rules/Api/ApiStaticCallRule.php +++ b/src/Rules/Api/ApiStaticCallRule.php @@ -8,6 +8,9 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function count; +use function sprintf; +use function strpos; /** * @implements Rule @@ -15,17 +18,11 @@ class ApiStaticCallRule implements Rule { - private ApiRuleHelper $apiRuleHelper; - - private ReflectionProvider $reflectionProvider; - public function __construct( - ApiRuleHelper $apiRuleHelper, - ReflectionProvider $reflectionProvider + private ApiRuleHelper $apiRuleHelper, + private ReflectionProvider $reflectionProvider, ) { - $this->apiRuleHelper = $apiRuleHelper; - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -67,10 +64,10 @@ public function processNode(Node $node, Scope $scope): array $ruleError = RuleErrorBuilder::message(sprintf( 'Calling %s::%s() is not covered by backward compatibility promise. The method might change in a minor PHPStan version.', $declaringClass->getDisplayName(), - $methodReflection->getName() + $methodReflection->getName(), ))->tip(sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", - 'https://github.com/phpstan/phpstan/discussions' + 'https://github.com/phpstan/phpstan/discussions', ))->build(); return [$ruleError]; diff --git a/src/Rules/Api/ApiTraitUseRule.php b/src/Rules/Api/ApiTraitUseRule.php index ec616163e5..bd2a7ae10c 100644 --- a/src/Rules/Api/ApiTraitUseRule.php +++ b/src/Rules/Api/ApiTraitUseRule.php @@ -7,6 +7,7 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function sprintf; /** * @implements Rule @@ -14,17 +15,11 @@ class ApiTraitUseRule implements Rule { - private ApiRuleHelper $apiRuleHelper; - - private ReflectionProvider $reflectionProvider; - public function __construct( - ApiRuleHelper $apiRuleHelper, - ReflectionProvider $reflectionProvider + private ApiRuleHelper $apiRuleHelper, + private ReflectionProvider $reflectionProvider, ) { - $this->apiRuleHelper = $apiRuleHelper; - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -37,7 +32,7 @@ public function processNode(Node $node, Scope $scope): array $errors = []; $tip = sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", - 'https://github.com/phpstan/phpstan/discussions' + 'https://github.com/phpstan/phpstan/discussions', ); foreach ($node->traits as $traitName) { $traitName = $traitName->toString(); @@ -52,7 +47,7 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( 'Using %s is not covered by backward compatibility promise. The trait might change in a minor PHPStan version.', - $traitReflection->getDisplayName() + $traitReflection->getDisplayName(), ))->tip($tip)->build(); } diff --git a/src/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRule.php b/src/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRule.php index 81f4ac4c54..892bbc1660 100644 --- a/src/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRule.php +++ b/src/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRule.php @@ -3,11 +3,17 @@ namespace PHPStan\Rules\Api; use Nette\Utils\Json; +use Nette\Utils\JsonException; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\File\CouldNotReadFileException; use PHPStan\File\FileReader; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function dirname; +use function is_dir; +use function is_file; +use function strpos; /** * @implements Rule @@ -15,11 +21,8 @@ class PhpStanNamespaceIn3rdPartyPackageRule implements Rule { - private ApiRuleHelper $apiRuleHelper; - - public function __construct(ApiRuleHelper $apiRuleHelper) + public function __construct(private ApiRuleHelper $apiRuleHelper) { - $this->apiRuleHelper = $apiRuleHelper; } public function getNodeType(): string @@ -65,14 +68,19 @@ private function findComposerJsonContents(string $fromDirectory): ?array $composerJsonPath = $fromDirectory . '/composer.json'; if (!is_file($composerJsonPath)) { - return $this->findComposerJsonContents(dirname($fromDirectory)); + $dirName = dirname($fromDirectory); + if ($dirName !== $fromDirectory) { + return $this->findComposerJsonContents($dirName); + } + + return null; } try { return Json::decode(FileReader::read($composerJsonPath), Json::FORCE_ARRAY); - } catch (\Nette\Utils\JsonException $e) { + } catch (JsonException) { return null; - } catch (\PHPStan\File\CouldNotReadFileException $e) { + } catch (CouldNotReadFileException) { return null; } } diff --git a/src/Rules/Arrays/AppendedArrayItemTypeRule.php b/src/Rules/Arrays/AppendedArrayItemTypeRule.php index 246a31b25c..396de12949 100644 --- a/src/Rules/Arrays/AppendedArrayItemTypeRule.php +++ b/src/Rules/Arrays/AppendedArrayItemTypeRule.php @@ -2,42 +2,40 @@ namespace PHPStan\Rules\Arrays; +use PhpParser\Node; use PhpParser\Node\Expr\ArrayDimFetch; use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\AssignOp; use PhpParser\Node\Expr\AssignRef; use PHPStan\Analyser\Scope; use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ArrayType; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr> + * @deprecated Replaced by PHPStan\Rules\Properties\TypesAssignedToPropertiesRule + * @implements Rule */ -class AppendedArrayItemTypeRule implements \PHPStan\Rules\Rule +class AppendedArrayItemTypeRule implements Rule { - private \PHPStan\Rules\Properties\PropertyReflectionFinder $propertyReflectionFinder; - - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - public function __construct( - PropertyReflectionFinder $propertyReflectionFinder, - RuleLevelHelper $ruleLevelHelper + private PropertyReflectionFinder $propertyReflectionFinder, + private RuleLevelHelper $ruleLevelHelper, ) { - $this->propertyReflectionFinder = $propertyReflectionFinder; - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string { - return \PhpParser\Node\Expr::class; + return Node\Expr::class; } - public function processNode(\PhpParser\Node $node, Scope $scope): array + public function processNode(Node $node, Scope $scope): array { if ( !$node instanceof Assign @@ -52,8 +50,8 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array } if ( - !$node->var->var instanceof \PhpParser\Node\Expr\PropertyFetch - && !$node->var->var instanceof \PhpParser\Node\Expr\StaticPropertyFetch + !$node->var->var instanceof Node\Expr\PropertyFetch + && !$node->var->var instanceof Node\Expr\StaticPropertyFetch ) { return []; } @@ -81,7 +79,7 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'Array (%s) does not accept %s.', $assignedToType->describe($verbosityLevel), - $assignedValueType->describe($verbosityLevel) + $assignedValueType->describe($verbosityLevel), ))->build(), ]; } diff --git a/src/Rules/Arrays/AppendedArrayKeyTypeRule.php b/src/Rules/Arrays/AppendedArrayKeyTypeRule.php index 1dd50f00c1..aa45ce31b4 100644 --- a/src/Rules/Arrays/AppendedArrayKeyTypeRule.php +++ b/src/Rules/Arrays/AppendedArrayKeyTypeRule.php @@ -2,33 +2,31 @@ namespace PHPStan\Rules\Arrays; +use PhpParser\Node; use PhpParser\Node\Expr\ArrayDimFetch; use PhpParser\Node\Expr\Assign; use PHPStan\Analyser\Scope; use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ArrayType; use PHPStan\Type\IntegerType; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Assign> + * @deprecated Replaced by PHPStan\Rules\Properties\TypesAssignedToPropertiesRule + * @implements Rule */ -class AppendedArrayKeyTypeRule implements \PHPStan\Rules\Rule +class AppendedArrayKeyTypeRule implements Rule { - private PropertyReflectionFinder $propertyReflectionFinder; - - private bool $checkUnionTypes; - public function __construct( - PropertyReflectionFinder $propertyReflectionFinder, - bool $checkUnionTypes + private PropertyReflectionFinder $propertyReflectionFinder, + private bool $checkUnionTypes, ) { - $this->propertyReflectionFinder = $propertyReflectionFinder; - $this->checkUnionTypes = $checkUnionTypes; } public function getNodeType(): string @@ -36,15 +34,15 @@ public function getNodeType(): string return Assign::class; } - public function processNode(\PhpParser\Node $node, Scope $scope): array + public function processNode(Node $node, Scope $scope): array { if (!($node->var instanceof ArrayDimFetch)) { return []; } if ( - !$node->var->var instanceof \PhpParser\Node\Expr\PropertyFetch - && !$node->var->var instanceof \PhpParser\Node\Expr\StaticPropertyFetch + !$node->var->var instanceof Node\Expr\PropertyFetch + && !$node->var->var instanceof Node\Expr\StaticPropertyFetch ) { return []; } @@ -76,11 +74,12 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array } if (!$arrayType->getIterableKeyType()->isSuperTypeOf($keyType)->yes()) { + $verbosity = VerbosityLevel::getRecommendedLevelByType($arrayType->getIterableKeyType(), $keyType); return [ RuleErrorBuilder::message(sprintf( 'Array (%s) does not accept key %s.', - $arrayType->describe(VerbosityLevel::typeOnly()), - $keyType->describe(VerbosityLevel::value()) + $arrayType->describe($verbosity), + $keyType->describe(VerbosityLevel::value()), ))->build(), ]; } diff --git a/src/Rules/Arrays/ArrayDestructuringRule.php b/src/Rules/Arrays/ArrayDestructuringRule.php index de523a58c3..37f63d4d7c 100644 --- a/src/Rules/Arrays/ArrayDestructuringRule.php +++ b/src/Rules/Arrays/ArrayDestructuringRule.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Arrays; +use ArrayAccess; use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\Node\Expr\Assign; @@ -14,8 +15,11 @@ use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ErrorType; +use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function array_merge; +use function sprintf; /** * @implements Rule @@ -23,17 +27,11 @@ class ArrayDestructuringRule implements Rule { - private RuleLevelHelper $ruleLevelHelper; - - private NonexistentOffsetInArrayDimFetchCheck $nonexistentOffsetInArrayDimFetchCheck; - public function __construct( - RuleLevelHelper $ruleLevelHelper, - NonexistentOffsetInArrayDimFetchCheck $nonexistentOffsetInArrayDimFetchCheck + private RuleLevelHelper $ruleLevelHelper, + private NonexistentOffsetInArrayDimFetchCheck $nonexistentOffsetInArrayDimFetchCheck, ) { - $this->ruleLevelHelper = $ruleLevelHelper; - $this->nonexistentOffsetInArrayDimFetchCheck = $nonexistentOffsetInArrayDimFetchCheck; } public function getNodeType(): string @@ -50,7 +48,7 @@ public function processNode(Node $node, Scope $scope): array return $this->getErrors( $scope, $node->var, - $node->expr + $node->expr, ); } @@ -64,15 +62,13 @@ private function getErrors(Scope $scope, Expr $var, Expr $expr): array $scope, $expr, '', - static function (Type $varType): bool { - return $varType->isArray()->yes(); - } + static fn (Type $varType): bool => $varType->isArray()->yes() || (new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->yes(), ); $exprType = $exprTypeResult->getType(); if ($exprType instanceof ErrorType) { return []; } - if (!$exprType->isArray()->yes()) { + if (!$exprType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($exprType)->yes()) { return [ RuleErrorBuilder::message(sprintf('Cannot use array destructuring on %s.', $exprType->describe(VerbosityLevel::typeOnly())))->build(), ]; @@ -103,7 +99,7 @@ static function (Type $varType): bool { $scope, $expr, '', - $keyType + $keyType, ); $errors = array_merge($errors, $itemErrors); @@ -120,7 +116,7 @@ static function (Type $varType): bool { $errors = array_merge($errors, $this->getErrors( $scope, $item->value, - new Expr\ArrayDimFetch($expr, $keyExpr) + new Expr\ArrayDimFetch($expr, $keyExpr), )); } diff --git a/src/Rules/Arrays/DeadForeachRule.php b/src/Rules/Arrays/DeadForeachRule.php index f24d930be3..0423c78fc5 100644 --- a/src/Rules/Arrays/DeadForeachRule.php +++ b/src/Rules/Arrays/DeadForeachRule.php @@ -8,7 +8,7 @@ use PHPStan\Rules\RuleErrorBuilder; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Foreach_> + * @implements Rule */ class DeadForeachRule implements Rule { diff --git a/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php b/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php index 72e17c0525..f3a856d3d2 100644 --- a/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php +++ b/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php @@ -2,24 +2,29 @@ namespace PHPStan\Rules\Arrays; +use PhpParser\Node; +use PhpParser\PrettyPrinter\Standard; use PHPStan\Analyser\Scope; use PHPStan\Node\LiteralArrayNode; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ConstantScalarType; +use function array_keys; +use function count; +use function implode; +use function sprintf; +use function var_export; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\LiteralArrayNode> + * @implements Rule */ -class DuplicateKeysInLiteralArraysRule implements \PHPStan\Rules\Rule +class DuplicateKeysInLiteralArraysRule implements Rule { - private \PhpParser\PrettyPrinter\Standard $printer; - public function __construct( - \PhpParser\PrettyPrinter\Standard $printer + private Standard $printer, ) { - $this->printer = $printer; } public function getNodeType(): string @@ -27,7 +32,7 @@ public function getNodeType(): string return LiteralArrayNode::class; } - public function processNode(\PhpParser\Node $node, Scope $scope): array + public function processNode(Node $node, Scope $scope): array { $values = []; $duplicateKeys = []; @@ -74,7 +79,7 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array count($printedValues[$value]), count($printedValues[$value]) === 1 ? 'duplicate key' : 'duplicate keys', var_export($value, true), - implode(', ', $printedValues[$value]) + implode(', ', $printedValues[$value]), ))->line($valueLines[$value])->build(); } diff --git a/src/Rules/Arrays/EmptyArrayItemRule.php b/src/Rules/Arrays/EmptyArrayItemRule.php index fa55e6a1f9..9bc3a395b2 100644 --- a/src/Rules/Arrays/EmptyArrayItemRule.php +++ b/src/Rules/Arrays/EmptyArrayItemRule.php @@ -9,7 +9,7 @@ use PHPStan\Rules\RuleErrorBuilder; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\LiteralArrayNode> + * @implements Rule */ class EmptyArrayItemRule implements Rule { diff --git a/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php b/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php index 18b771b859..7e0d752e99 100644 --- a/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php +++ b/src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php @@ -2,31 +2,32 @@ namespace PHPStan\Rules\Arrays; +use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; use PHPStan\Type\TypeUtils; use PHPStan\Type\VerbosityLevel; +use function count; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\ArrayDimFetch> + * @implements Rule */ -class InvalidKeyInArrayDimFetchRule implements \PHPStan\Rules\Rule +class InvalidKeyInArrayDimFetchRule implements Rule { - private bool $reportMaybes; - - public function __construct(bool $reportMaybes) + public function __construct(private bool $reportMaybes) { - $this->reportMaybes = $reportMaybes; } public function getNodeType(): string { - return \PhpParser\Node\Expr\ArrayDimFetch::class; + return Node\Expr\ArrayDimFetch::class; } - public function processNode(\PhpParser\Node $node, Scope $scope): array + public function processNode(Node $node, Scope $scope): array { if ($node->dim === null) { return []; @@ -42,13 +43,13 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array if ($isSuperType->no()) { return [ RuleErrorBuilder::message( - sprintf('Invalid array key type %s.', $dimensionType->describe(VerbosityLevel::typeOnly())) + sprintf('Invalid array key type %s.', $dimensionType->describe(VerbosityLevel::typeOnly())), )->build(), ]; } elseif ($this->reportMaybes && $isSuperType->maybe() && !$dimensionType instanceof MixedType) { return [ RuleErrorBuilder::message( - sprintf('Possibly invalid array key type %s.', $dimensionType->describe(VerbosityLevel::typeOnly())) + sprintf('Possibly invalid array key type %s.', $dimensionType->describe(VerbosityLevel::typeOnly())), )->build(), ]; } diff --git a/src/Rules/Arrays/InvalidKeyInArrayItemRule.php b/src/Rules/Arrays/InvalidKeyInArrayItemRule.php index 1356bc501c..b692bbe15a 100644 --- a/src/Rules/Arrays/InvalidKeyInArrayItemRule.php +++ b/src/Rules/Arrays/InvalidKeyInArrayItemRule.php @@ -2,30 +2,30 @@ namespace PHPStan\Rules\Arrays; +use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\ArrayItem> + * @implements Rule */ -class InvalidKeyInArrayItemRule implements \PHPStan\Rules\Rule +class InvalidKeyInArrayItemRule implements Rule { - private bool $reportMaybes; - - public function __construct(bool $reportMaybes) + public function __construct(private bool $reportMaybes) { - $this->reportMaybes = $reportMaybes; } public function getNodeType(): string { - return \PhpParser\Node\Expr\ArrayItem::class; + return Node\Expr\ArrayItem::class; } - public function processNode(\PhpParser\Node $node, Scope $scope): array + public function processNode(Node $node, Scope $scope): array { if ($node->key === null) { return []; @@ -36,13 +36,13 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array if ($isSuperType->no()) { return [ RuleErrorBuilder::message( - sprintf('Invalid array key type %s.', $dimensionType->describe(VerbosityLevel::typeOnly())) + sprintf('Invalid array key type %s.', $dimensionType->describe(VerbosityLevel::typeOnly())), )->build(), ]; } elseif ($this->reportMaybes && $isSuperType->maybe() && !$dimensionType instanceof MixedType) { return [ RuleErrorBuilder::message( - sprintf('Possibly invalid array key type %s.', $dimensionType->describe(VerbosityLevel::typeOnly())) + sprintf('Possibly invalid array key type %s.', $dimensionType->describe(VerbosityLevel::typeOnly())), )->build(), ]; } diff --git a/src/Rules/Arrays/IterableInForeachRule.php b/src/Rules/Arrays/IterableInForeachRule.php index dafc1c56d7..57fd2080f3 100644 --- a/src/Rules/Arrays/IterableInForeachRule.php +++ b/src/Rules/Arrays/IterableInForeachRule.php @@ -4,39 +4,38 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\InForeachNode; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Foreach_> + * @implements Rule */ -class IterableInForeachRule implements \PHPStan\Rules\Rule +class IterableInForeachRule implements Rule { - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - public function __construct(RuleLevelHelper $ruleLevelHelper) + public function __construct(private RuleLevelHelper $ruleLevelHelper) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string { - return \PhpParser\Node\Stmt\Foreach_::class; + return InForeachNode::class; } public function processNode(Node $node, Scope $scope): array { + $originalNode = $node->getOriginalNode(); $typeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, - $node->expr, + $originalNode->expr, 'Iterating over an object of an unknown class %s.', - static function (Type $type): bool { - return $type->isIterable()->yes(); - } + static fn (Type $type): bool => $type->isIterable()->yes(), ); $type = $typeResult->getType(); if ($type instanceof ErrorType) { @@ -49,8 +48,8 @@ static function (Type $type): bool { return [ RuleErrorBuilder::message(sprintf( 'Argument of an invalid type %s supplied for foreach, only iterables are supported.', - $type->describe(VerbosityLevel::typeOnly()) - ))->line($node->expr->getLine())->build(), + $type->describe(VerbosityLevel::typeOnly()), + ))->line($originalNode->expr->getLine())->build(), ]; } diff --git a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php index f9e5c2f32f..ae4847e61e 100644 --- a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php +++ b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Arrays; use PhpParser\Node\Expr; +use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; @@ -13,41 +14,31 @@ use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function count; +use function sprintf; class NonexistentOffsetInArrayDimFetchCheck { - private RuleLevelHelper $ruleLevelHelper; - - private bool $reportMaybes; - - public function __construct(RuleLevelHelper $ruleLevelHelper, bool $reportMaybes) + public function __construct(private RuleLevelHelper $ruleLevelHelper, private bool $reportMaybes) { - $this->ruleLevelHelper = $ruleLevelHelper; - $this->reportMaybes = $reportMaybes; } /** - * @param Scope $scope - * @param Expr $var - * @param string $unknownClassPattern - * @param Type $dimType * @return RuleError[] */ public function check( Scope $scope, Expr $var, string $unknownClassPattern, - Type $dimType + Type $dimType, ): array { $typeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, - $var, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $var), $unknownClassPattern, - static function (Type $type) use ($dimType): bool { - return $type->hasOffsetValueType($dimType)->yes(); - } + static fn (Type $type): bool => $type->hasOffsetValueType($dimType)->yes(), ); $type = $typeResult->getType(); if ($type instanceof ErrorType) { diff --git a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php index e5f4e9dd8b..7a8a66490c 100644 --- a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php +++ b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php @@ -2,46 +2,42 @@ namespace PHPStan\Rules\Arrays; +use PhpParser\Node; +use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\ArrayDimFetch> + * @implements Rule */ -class NonexistentOffsetInArrayDimFetchRule implements \PHPStan\Rules\Rule +class NonexistentOffsetInArrayDimFetchRule implements Rule { - private RuleLevelHelper $ruleLevelHelper; - - private NonexistentOffsetInArrayDimFetchCheck $nonexistentOffsetInArrayDimFetchCheck; - - private bool $reportMaybes; - public function __construct( - RuleLevelHelper $ruleLevelHelper, - NonexistentOffsetInArrayDimFetchCheck $nonexistentOffsetInArrayDimFetchCheck, - bool $reportMaybes + private RuleLevelHelper $ruleLevelHelper, + private NonexistentOffsetInArrayDimFetchCheck $nonexistentOffsetInArrayDimFetchCheck, + private bool $reportMaybes, ) { - $this->ruleLevelHelper = $ruleLevelHelper; - $this->nonexistentOffsetInArrayDimFetchCheck = $nonexistentOffsetInArrayDimFetchCheck; - $this->reportMaybes = $reportMaybes; } public function getNodeType(): string { - return \PhpParser\Node\Expr\ArrayDimFetch::class; + return Node\Expr\ArrayDimFetch::class; } - public function processNode(\PhpParser\Node $node, Scope $scope): array + public function processNode(Node $node, Scope $scope): array { if ($node->dim !== null) { $dimType = $scope->getType($node->dim); - $unknownClassPattern = sprintf('Access to offset %s on an unknown class %%s.', $dimType->describe(VerbosityLevel::value())); + $unknownClassPattern = sprintf('Access to offset %s on an unknown class %%s.', SprintfHelper::escapeFormatString($dimType->describe(VerbosityLevel::value()))); } else { $dimType = null; $unknownClassPattern = 'Access to an offset on an unknown class %s.'; @@ -49,11 +45,9 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array $isOffsetAccessibleTypeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, - $node->var, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->var), $unknownClassPattern, - static function (Type $type): bool { - return $type->isOffsetAccessible()->yes(); - } + static fn (Type $type): bool => $type->isOffsetAccessible()->yes(), ); $isOffsetAccessibleType = $isOffsetAccessibleTypeResult->getType(); if ($isOffsetAccessibleType instanceof ErrorType) { @@ -73,7 +67,7 @@ static function (Type $type): bool { RuleErrorBuilder::message(sprintf( 'Cannot access offset %s on %s.', $dimType->describe(VerbosityLevel::value()), - $isOffsetAccessibleType->describe(VerbosityLevel::value()) + $isOffsetAccessibleType->describe(VerbosityLevel::value()), ))->build(), ]; } @@ -81,7 +75,7 @@ static function (Type $type): bool { return [ RuleErrorBuilder::message(sprintf( 'Cannot access an offset on %s.', - $isOffsetAccessibleType->describe(VerbosityLevel::typeOnly()) + $isOffsetAccessibleType->describe(VerbosityLevel::typeOnly()), ))->build(), ]; } @@ -97,7 +91,7 @@ static function (Type $type): bool { $scope, $node->var, $unknownClassPattern, - $dimType + $dimType, ); } diff --git a/src/Rules/Arrays/OffsetAccessAssignOpRule.php b/src/Rules/Arrays/OffsetAccessAssignOpRule.php index 054d988fbd..46e3e1fb4e 100644 --- a/src/Rules/Arrays/OffsetAccessAssignOpRule.php +++ b/src/Rules/Arrays/OffsetAccessAssignOpRule.php @@ -2,34 +2,34 @@ namespace PHPStan\Rules\Arrays; +use PhpParser\Node; use PhpParser\Node\Expr\ArrayDimFetch; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\AssignOp> + * @implements Rule */ -class OffsetAccessAssignOpRule implements \PHPStan\Rules\Rule +class OffsetAccessAssignOpRule implements Rule { - private RuleLevelHelper $ruleLevelHelper; - - public function __construct(RuleLevelHelper $ruleLevelHelper) + public function __construct(private RuleLevelHelper $ruleLevelHelper) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string { - return \PhpParser\Node\Expr\AssignOp::class; + return Node\Expr\AssignOp::class; } - public function processNode(\PhpParser\Node $node, Scope $scope): array + public function processNode(Node $node, Scope $scope): array { if (!$node->var instanceof ArrayDimFetch) { return []; @@ -49,7 +49,7 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array static function (Type $varType) use ($potentialDimType): bool { $arrayDimType = $varType->setOffsetValueType($potentialDimType, new MixedType()); return !($arrayDimType instanceof ErrorType); - } + }, ); $varType = $varTypeResult->getType(); @@ -61,7 +61,7 @@ static function (Type $varType) use ($potentialDimType): bool { static function (Type $dimType) use ($varType): bool { $arrayDimType = $varType->setOffsetValueType($dimType, new MixedType()); return !($arrayDimType instanceof ErrorType); - } + }, ); $dimType = $dimTypeResult->getType(); if ($varType->hasOffsetValueType($dimType)->no()) { @@ -80,7 +80,7 @@ static function (Type $dimType) use ($varType): bool { return [ RuleErrorBuilder::message(sprintf( 'Cannot assign new offset to %s.', - $varType->describe(VerbosityLevel::typeOnly()) + $varType->describe(VerbosityLevel::typeOnly()), ))->build(), ]; } @@ -89,7 +89,7 @@ static function (Type $dimType) use ($varType): bool { RuleErrorBuilder::message(sprintf( 'Cannot assign offset %s to %s.', $dimType->describe(VerbosityLevel::value()), - $varType->describe(VerbosityLevel::typeOnly()) + $varType->describe(VerbosityLevel::typeOnly()), ))->build(), ]; } diff --git a/src/Rules/Arrays/OffsetAccessAssignmentRule.php b/src/Rules/Arrays/OffsetAccessAssignmentRule.php index e111fca026..ec4ca4dc9c 100644 --- a/src/Rules/Arrays/OffsetAccessAssignmentRule.php +++ b/src/Rules/Arrays/OffsetAccessAssignmentRule.php @@ -2,33 +2,34 @@ namespace PHPStan\Rules\Arrays; +use PhpParser\Node; +use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\ArrayDimFetch> + * @implements Rule */ -class OffsetAccessAssignmentRule implements \PHPStan\Rules\Rule +class OffsetAccessAssignmentRule implements Rule { - private RuleLevelHelper $ruleLevelHelper; - - public function __construct(RuleLevelHelper $ruleLevelHelper) + public function __construct(private RuleLevelHelper $ruleLevelHelper) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string { - return \PhpParser\Node\Expr\ArrayDimFetch::class; + return Node\Expr\ArrayDimFetch::class; } - public function processNode(\PhpParser\Node $node, Scope $scope): array + public function processNode(Node $node, Scope $scope): array { if (!$scope->isInExpressionAssign($node)) { return []; @@ -41,12 +42,12 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array $varTypeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, - $node->var, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->var), '', static function (Type $varType) use ($potentialDimType): bool { $arrayDimType = $varType->setOffsetValueType($potentialDimType, new MixedType()); return !($arrayDimType instanceof ErrorType); - } + }, ); $varType = $varTypeResult->getType(); if ($varType instanceof ErrorType) { @@ -64,7 +65,7 @@ static function (Type $varType) use ($potentialDimType): bool { static function (Type $dimType) use ($varType): bool { $arrayDimType = $varType->setOffsetValueType($dimType, new MixedType()); return !($arrayDimType instanceof ErrorType); - } + }, ); $dimType = $dimTypeResult->getType(); } else { @@ -80,7 +81,7 @@ static function (Type $dimType) use ($varType): bool { return [ RuleErrorBuilder::message(sprintf( 'Cannot assign new offset to %s.', - $varType->describe(VerbosityLevel::typeOnly()) + $varType->describe(VerbosityLevel::typeOnly()), ))->build(), ]; } @@ -89,7 +90,7 @@ static function (Type $dimType) use ($varType): bool { RuleErrorBuilder::message(sprintf( 'Cannot assign offset %s to %s.', $dimType->describe(VerbosityLevel::value()), - $varType->describe(VerbosityLevel::typeOnly()) + $varType->describe(VerbosityLevel::typeOnly()), ))->build(), ]; } diff --git a/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php b/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php index ea571f6499..47d58c87fa 100644 --- a/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php +++ b/src/Rules/Arrays/OffsetAccessValueAssignmentRule.php @@ -2,28 +2,28 @@ namespace PHPStan\Rules\Arrays; +use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\AssignOp; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -class OffsetAccessValueAssignmentRule implements \PHPStan\Rules\Rule +class OffsetAccessValueAssignmentRule implements Rule { - private RuleLevelHelper $ruleLevelHelper; - - public function __construct(RuleLevelHelper $ruleLevelHelper) + public function __construct(private RuleLevelHelper $ruleLevelHelper) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string @@ -31,7 +31,7 @@ public function getNodeType(): string return Expr::class; } - public function processNode(\PhpParser\Node $node, Scope $scope): array + public function processNode(Node $node, Scope $scope): array { if ( !$node instanceof Assign @@ -61,7 +61,7 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array static function (Type $varType) use ($assignedValueType): bool { $result = $varType->setOffsetValueType(new MixedType(), $assignedValueType); return !($result instanceof ErrorType); - } + }, ); $arrayType = $arrayTypeResult->getType(); if ($arrayType instanceof ErrorType) { @@ -80,7 +80,7 @@ static function (Type $varType) use ($assignedValueType): bool { RuleErrorBuilder::message(sprintf( '%s does not accept %s.', $originalArrayType->describe(VerbosityLevel::value()), - $assignedValueType->describe(VerbosityLevel::typeOnly()) + $assignedValueType->describe(VerbosityLevel::typeOnly()), ))->build(), ]; } diff --git a/src/Rules/Arrays/OffsetAccessWithoutDimForReadingRule.php b/src/Rules/Arrays/OffsetAccessWithoutDimForReadingRule.php index cfc5fdb66c..2f93bd119f 100644 --- a/src/Rules/Arrays/OffsetAccessWithoutDimForReadingRule.php +++ b/src/Rules/Arrays/OffsetAccessWithoutDimForReadingRule.php @@ -2,21 +2,23 @@ namespace PHPStan\Rules\Arrays; +use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\ArrayDimFetch> + * @implements Rule */ -class OffsetAccessWithoutDimForReadingRule implements \PHPStan\Rules\Rule +class OffsetAccessWithoutDimForReadingRule implements Rule { public function getNodeType(): string { - return \PhpParser\Node\Expr\ArrayDimFetch::class; + return Node\Expr\ArrayDimFetch::class; } - public function processNode(\PhpParser\Node $node, Scope $scope): array + public function processNode(Node $node, Scope $scope): array { if ($scope->isInExpressionAssign($node)) { return []; diff --git a/src/Rules/Arrays/UnpackIterableInArrayRule.php b/src/Rules/Arrays/UnpackIterableInArrayRule.php index a061c670a2..08ba2b7e84 100644 --- a/src/Rules/Arrays/UnpackIterableInArrayRule.php +++ b/src/Rules/Arrays/UnpackIterableInArrayRule.php @@ -11,20 +11,18 @@ use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\LiteralArrayNode> + * @implements Rule */ class UnpackIterableInArrayRule implements Rule { - private RuleLevelHelper $ruleLevelHelper; - public function __construct( - RuleLevelHelper $ruleLevelHelper + private RuleLevelHelper $ruleLevelHelper, ) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string @@ -48,9 +46,7 @@ public function processNode(Node $node, Scope $scope): array $scope, $item->value, '', - static function (Type $type): bool { - return $type->isIterable()->yes(); - } + static fn (Type $type): bool => $type->isIterable()->yes(), ); $type = $typeResult->getType(); if ($type instanceof ErrorType) { @@ -63,7 +59,7 @@ static function (Type $type): bool { $errors[] = RuleErrorBuilder::message(sprintf( 'Only iterables can be unpacked, %s given.', - $type->describe(VerbosityLevel::typeOnly()) + $type->describe(VerbosityLevel::typeOnly()), ))->line($item->getLine())->build(); } diff --git a/src/Rules/AttributesCheck.php b/src/Rules/AttributesCheck.php index c9080f772f..1349fe5c35 100644 --- a/src/Rules/AttributesCheck.php +++ b/src/Rules/AttributesCheck.php @@ -2,42 +2,39 @@ namespace PHPStan\Rules; +use Attribute; use PhpParser\Node\AttributeGroup; use PhpParser\Node\Expr\New_; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; +use function array_key_exists; +use function count; +use function sprintf; +use function strtolower; class AttributesCheck { - private ReflectionProvider $reflectionProvider; - - private FunctionCallParametersCheck $functionCallParametersCheck; - - private ClassCaseSensitivityCheck $classCaseSensitivityCheck; - public function __construct( - ReflectionProvider $reflectionProvider, - FunctionCallParametersCheck $functionCallParametersCheck, - ClassCaseSensitivityCheck $classCaseSensitivityCheck + private ReflectionProvider $reflectionProvider, + private FunctionCallParametersCheck $functionCallParametersCheck, + private ClassCaseSensitivityCheck $classCaseSensitivityCheck, ) { - $this->reflectionProvider = $reflectionProvider; - $this->functionCallParametersCheck = $functionCallParametersCheck; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; } /** * @param AttributeGroup[] $attrGroups - * @param \Attribute::TARGET_* $requiredTarget + * @param Attribute::TARGET_* $requiredTarget * @return RuleError[] */ public function check( Scope $scope, array $attrGroups, int $requiredTarget, - string $targetName + string $targetName, ): array { $errors = []; @@ -52,7 +49,16 @@ public function check( $attributeClass = $this->reflectionProvider->getClass($name); if (!$attributeClass->isAttributeClass()) { - $errors[] = RuleErrorBuilder::message(sprintf('Class %s is not an Attribute class.', $attributeClass->getDisplayName()))->line($attribute->getLine())->build(); + $classLikeDescription = 'Class'; + if ($attributeClass->isInterface()) { + $classLikeDescription = 'Interface'; + } elseif ($attributeClass->isTrait()) { + $classLikeDescription = 'Trait'; + } elseif ($attributeClass->isEnum()) { + $classLikeDescription = 'Enum'; + } + + $errors[] = RuleErrorBuilder::message(sprintf('%s %s is not an Attribute class.', $classLikeDescription, $attributeClass->getDisplayName()))->line($attribute->getLine())->build(); continue; } @@ -69,7 +75,7 @@ public function check( $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s does not have the %s target.', $name, $targetName))->line($attribute->getLine())->build(); } - if (($flags & \Attribute::IS_REPEATABLE) === 0) { + if (($flags & Attribute::IS_REPEATABLE) === 0) { $loweredName = strtolower($name); if (array_key_exists($loweredName, $alreadyPresent)) { $errors[] = RuleErrorBuilder::message(sprintf('Attribute class %s is not repeatable but is already present above the %s.', $name, $targetName))->line($attribute->getLine())->build(); @@ -90,26 +96,31 @@ public function check( $errors[] = RuleErrorBuilder::message(sprintf('Constructor of attribute class %s is not public.', $name))->line($attribute->getLine())->build(); } + $attributeClassName = SprintfHelper::escapeFormatString($attributeClass->getDisplayName()); + + $nodeAttributes = $attribute->getAttributes(); + $nodeAttributes['isAttribute'] = true; + $parameterErrors = $this->functionCallParametersCheck->check( ParametersAcceptorSelector::selectSingle($attributeConstructor->getVariants()), $scope, $attributeConstructor->getDeclaringClass()->isBuiltin(), - new New_($attribute->name, $attribute->args, $attribute->getAttributes()), + new New_($attribute->name, $attribute->args, $nodeAttributes), [ - 'Attribute class ' . $attributeClass->getDisplayName() . ' constructor invoked with %d parameter, %d required.', - 'Attribute class ' . $attributeClass->getDisplayName() . ' constructor invoked with %d parameters, %d required.', - 'Attribute class ' . $attributeClass->getDisplayName() . ' constructor invoked with %d parameter, at least %d required.', - 'Attribute class ' . $attributeClass->getDisplayName() . ' constructor invoked with %d parameters, at least %d required.', - 'Attribute class ' . $attributeClass->getDisplayName() . ' constructor invoked with %d parameter, %d-%d required.', - 'Attribute class ' . $attributeClass->getDisplayName() . ' constructor invoked with %d parameters, %d-%d required.', - 'Parameter %s of attribute class ' . $attributeClass->getDisplayName() . ' constructor expects %s, %s given.', + 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameter, %d required.', + 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameters, %d required.', + 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameter, at least %d required.', + 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameters, at least %d required.', + 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameter, %d-%d required.', + 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameters, %d-%d required.', + 'Parameter %s of attribute class ' . $attributeClassName . ' constructor expects %s, %s given.', '', // constructor does not have a return type - 'Parameter %s of attribute class ' . $attributeClass->getDisplayName() . ' constructor is passed by reference, so it expects variables only', - 'Unable to resolve the template type %s in instantiation of attribute class ' . $attributeClass->getDisplayName(), - 'Missing parameter $%s in call to ' . $attributeClass->getDisplayName() . ' constructor.', - 'Unknown parameter $%s in call to ' . $attributeClass->getDisplayName() . ' constructor.', - 'Return type of call to ' . $attributeClass->getDisplayName() . ' constructor contains unresolvable type.', - ] + 'Parameter %s of attribute class ' . $attributeClassName . ' constructor is passed by reference, so it expects variables only', + 'Unable to resolve the template type %s in instantiation of attribute class ' . $attributeClassName, + 'Missing parameter $%s in call to ' . $attributeClassName . ' constructor.', + 'Unknown parameter $%s in call to ' . $attributeClassName . ' constructor.', + 'Return type of call to ' . $attributeClassName . ' constructor contains unresolvable type.', + ], ); foreach ($parameterErrors as $error) { diff --git a/src/Rules/Cast/EchoRule.php b/src/Rules/Cast/EchoRule.php index b0ede225b3..705dc48ebd 100644 --- a/src/Rules/Cast/EchoRule.php +++ b/src/Rules/Cast/EchoRule.php @@ -10,18 +10,16 @@ use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Echo_> + * @implements Rule */ class EchoRule implements Rule { - private RuleLevelHelper $ruleLevelHelper; - - public function __construct(RuleLevelHelper $ruleLevelHelper) + public function __construct(private RuleLevelHelper $ruleLevelHelper) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string @@ -38,9 +36,7 @@ public function processNode(Node $node, Scope $scope): array $scope, $expr, '', - static function (Type $type): bool { - return !$type->toString() instanceof ErrorType; - } + static fn (Type $type): bool => !$type->toString() instanceof ErrorType, ); if ($typeResult->getType() instanceof ErrorType @@ -52,7 +48,7 @@ static function (Type $type): bool { $messages[] = RuleErrorBuilder::message(sprintf( 'Parameter #%d (%s) of echo cannot be converted to string.', $key + 1, - $typeResult->getType()->describe(VerbosityLevel::value()) + $typeResult->getType()->describe(VerbosityLevel::value()), ))->line($expr->getLine())->build(); } return $messages; diff --git a/src/Rules/Cast/InvalidCastRule.php b/src/Rules/Cast/InvalidCastRule.php index 7a2e63be5f..4179390523 100644 --- a/src/Rules/Cast/InvalidCastRule.php +++ b/src/Rules/Cast/InvalidCastRule.php @@ -5,46 +5,45 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function get_class; +use function sprintf; +use function strtolower; +use function substr; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Cast> + * @implements Rule */ -class InvalidCastRule implements \PHPStan\Rules\Rule +class InvalidCastRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - public function __construct( - ReflectionProvider $reflectionProvider, - RuleLevelHelper $ruleLevelHelper + private ReflectionProvider $reflectionProvider, + private RuleLevelHelper $ruleLevelHelper, ) { - $this->reflectionProvider = $reflectionProvider; - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string { - return \PhpParser\Node\Expr\Cast::class; + return Node\Expr\Cast::class; } public function processNode(Node $node, Scope $scope): array { $castTypeCallback = static function (Type $type) use ($node): ?Type { - if ($node instanceof \PhpParser\Node\Expr\Cast\Int_) { + if ($node instanceof Node\Expr\Cast\Int_) { return $type->toInteger(); - } elseif ($node instanceof \PhpParser\Node\Expr\Cast\Bool_) { + } elseif ($node instanceof Node\Expr\Cast\Bool_) { return $type->toBoolean(); - } elseif ($node instanceof \PhpParser\Node\Expr\Cast\Double) { + } elseif ($node instanceof Node\Expr\Cast\Double) { return $type->toFloat(); - } elseif ($node instanceof \PhpParser\Node\Expr\Cast\String_) { + } elseif ($node instanceof Node\Expr\Cast\String_) { return $type->toString(); } @@ -62,7 +61,7 @@ static function (Type $type) use ($castTypeCallback): bool { } return !$castType instanceof ErrorType; - } + }, ); $type = $typeResult->getType(); if ($type instanceof ErrorType) { @@ -84,7 +83,7 @@ static function (Type $type) use ($castTypeCallback): bool { RuleErrorBuilder::message(sprintf( 'Cannot cast %s to %s.', $scope->getType($node->expr)->describe(VerbosityLevel::value()), - $shortName + $shortName, ))->line($node->getLine())->build(), ]; } diff --git a/src/Rules/Cast/InvalidPartOfEncapsedStringRule.php b/src/Rules/Cast/InvalidPartOfEncapsedStringRule.php index 54bb38829a..293adeac5a 100644 --- a/src/Rules/Cast/InvalidPartOfEncapsedStringRule.php +++ b/src/Rules/Cast/InvalidPartOfEncapsedStringRule.php @@ -3,35 +3,32 @@ namespace PHPStan\Rules\Cast; use PhpParser\Node; +use PhpParser\PrettyPrinter\Standard; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Scalar\Encapsed> + * @implements Rule */ -class InvalidPartOfEncapsedStringRule implements \PHPStan\Rules\Rule +class InvalidPartOfEncapsedStringRule implements Rule { - private \PhpParser\PrettyPrinter\Standard $printer; - - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - public function __construct( - \PhpParser\PrettyPrinter\Standard $printer, - RuleLevelHelper $ruleLevelHelper + private Standard $printer, + private RuleLevelHelper $ruleLevelHelper, ) { - $this->printer = $printer; - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string { - return \PhpParser\Node\Scalar\Encapsed::class; + return Node\Scalar\Encapsed::class; } public function processNode(Node $node, Scope $scope): array @@ -46,9 +43,7 @@ public function processNode(Node $node, Scope $scope): array $scope, $part, '', - static function (Type $type): bool { - return !$type->toString() instanceof ErrorType; - } + static fn (Type $type): bool => !$type->toString() instanceof ErrorType, ); $partType = $typeResult->getType(); if ($partType instanceof ErrorType) { @@ -62,7 +57,7 @@ static function (Type $type): bool { $messages[] = RuleErrorBuilder::message(sprintf( 'Part %s (%s) of encapsed string cannot be cast to string.', $this->printer->prettyPrintExpr($part), - $partType->describe(VerbosityLevel::value()) + $partType->describe(VerbosityLevel::value()), ))->line($part->getLine())->build(); } diff --git a/src/Rules/Cast/PrintRule.php b/src/Rules/Cast/PrintRule.php index 3e719af4b5..e511f76ebf 100644 --- a/src/Rules/Cast/PrintRule.php +++ b/src/Rules/Cast/PrintRule.php @@ -10,18 +10,16 @@ use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Print_> + * @implements Rule */ class PrintRule implements Rule { - private RuleLevelHelper $ruleLevelHelper; - - public function __construct(RuleLevelHelper $ruleLevelHelper) + public function __construct(private RuleLevelHelper $ruleLevelHelper) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string @@ -35,9 +33,7 @@ public function processNode(Node $node, Scope $scope): array $scope, $node->expr, '', - static function (Type $type): bool { - return !$type->toString() instanceof ErrorType; - } + static fn (Type $type): bool => !$type->toString() instanceof ErrorType, ); if (!$typeResult->getType() instanceof ErrorType @@ -45,7 +41,7 @@ static function (Type $type): bool { ) { return [RuleErrorBuilder::message(sprintf( 'Parameter %s of print cannot be converted to string.', - $typeResult->getType()->describe(VerbosityLevel::value()) + $typeResult->getType()->describe(VerbosityLevel::value()), ))->line($node->expr->getLine())->build()]; } diff --git a/src/Rules/Cast/UnsetCastRule.php b/src/Rules/Cast/UnsetCastRule.php index c50aca654a..6260d9805b 100644 --- a/src/Rules/Cast/UnsetCastRule.php +++ b/src/Rules/Cast/UnsetCastRule.php @@ -14,11 +14,8 @@ class UnsetCastRule implements Rule { - private PhpVersion $phpVersion; - - public function __construct(PhpVersion $phpVersion) + public function __construct(private PhpVersion $phpVersion) { - $this->phpVersion = $phpVersion; } public function getNodeType(): string diff --git a/src/Rules/ClassCaseSensitivityCheck.php b/src/Rules/ClassCaseSensitivityCheck.php index e3e1e67cda..cb3f572609 100644 --- a/src/Rules/ClassCaseSensitivityCheck.php +++ b/src/Rules/ClassCaseSensitivityCheck.php @@ -4,18 +4,14 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ReflectionProvider; +use function sprintf; +use function strtolower; class ClassCaseSensitivityCheck { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private bool $checkInternalClassCaseSensitivity; - - public function __construct(ReflectionProvider $reflectionProvider, bool $checkInternalClassCaseSensitivity = false) + public function __construct(private ReflectionProvider $reflectionProvider, private bool $checkInternalClassCaseSensitivity) { - $this->reflectionProvider = $reflectionProvider; - $this->checkInternalClassCaseSensitivity = $checkInternalClassCaseSensitivity; } /** @@ -46,7 +42,7 @@ public function checkClassNames(array $pairs): array '%s %s referenced with incorrect case: %s.', $this->getTypeName($classReflection), $realClassName, - $className + $className, ))->line($pair->getNode()->getLine())->build(); } @@ -59,6 +55,8 @@ private function getTypeName(ClassReflection $classReflection): string return 'Interface'; } elseif ($classReflection->isTrait()) { return 'Trait'; + } elseif ($classReflection->isEnum()) { + return 'Enum'; } return 'Class'; diff --git a/src/Rules/ClassNameNodePair.php b/src/Rules/ClassNameNodePair.php index a92f539071..355fa61e01 100644 --- a/src/Rules/ClassNameNodePair.php +++ b/src/Rules/ClassNameNodePair.php @@ -7,14 +7,8 @@ class ClassNameNodePair { - private string $className; - - private Node $node; - - public function __construct(string $className, Node $node) + public function __construct(private string $className, private Node $node) { - $this->className = $className; - $this->node = $node; } public function getClassName(): string diff --git a/src/Rules/Classes/AccessPrivateConstantThroughStaticRule.php b/src/Rules/Classes/AccessPrivateConstantThroughStaticRule.php new file mode 100644 index 0000000000..a2ca28b670 --- /dev/null +++ b/src/Rules/Classes/AccessPrivateConstantThroughStaticRule.php @@ -0,0 +1,61 @@ + + */ +class AccessPrivateConstantThroughStaticRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Expr\ClassConstFetch::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Identifier) { + return []; + } + if (!$node->class instanceof Name) { + return []; + } + + $constantName = $node->name->name; + $className = $node->class; + if ($className->toLowerString() !== 'static') { + return []; + } + + $classType = $scope->resolveTypeByName($className); + if (!$classType->hasConstant($constantName)->yes()) { + return []; + } + + $constant = $classType->getConstant($constantName); + if (!$constant->isPrivate()) { + return []; + } + + if ($scope->isInClass() && $scope->getClassReflection()->isFinal()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Unsafe access to private constant %s::%s through static::.', + $constant->getDeclaringClass()->getDisplayName(), + $constantName, + ))->build(), + ]; + } + +} diff --git a/src/Rules/Classes/ClassAttributesRule.php b/src/Rules/Classes/ClassAttributesRule.php index 839a8fd112..21d6bda749 100644 --- a/src/Rules/Classes/ClassAttributesRule.php +++ b/src/Rules/Classes/ClassAttributesRule.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Classes; +use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Rules\AttributesCheck; @@ -13,11 +14,8 @@ class ClassAttributesRule implements Rule { - private AttributesCheck $attributesCheck; - - public function __construct(AttributesCheck $attributesCheck) + public function __construct(private AttributesCheck $attributesCheck) { - $this->attributesCheck = $attributesCheck; } public function getNodeType(): string @@ -30,8 +28,8 @@ public function processNode(Node $node, Scope $scope): array return $this->attributesCheck->check( $scope, $node->attrGroups, - \Attribute::TARGET_CLASS, - 'class' + Attribute::TARGET_CLASS, + 'class', ); } diff --git a/src/Rules/Classes/ClassConstantAttributesRule.php b/src/Rules/Classes/ClassConstantAttributesRule.php index ded689c6ee..d8256191a0 100644 --- a/src/Rules/Classes/ClassConstantAttributesRule.php +++ b/src/Rules/Classes/ClassConstantAttributesRule.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Classes; +use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Rules\AttributesCheck; @@ -13,11 +14,8 @@ class ClassConstantAttributesRule implements Rule { - private AttributesCheck $attributesCheck; - - public function __construct(AttributesCheck $attributesCheck) + public function __construct(private AttributesCheck $attributesCheck) { - $this->attributesCheck = $attributesCheck; } public function getNodeType(): string @@ -30,8 +28,8 @@ public function processNode(Node $node, Scope $scope): array return $this->attributesCheck->check( $scope, $node->attrGroups, - \Attribute::TARGET_CLASS_CONSTANT, - 'class constant' + Attribute::TARGET_CLASS_CONSTANT, + 'class constant', ); } diff --git a/src/Rules/Classes/ClassConstantRule.php b/src/Rules/Classes/ClassConstantRule.php index 64d4e11bec..98aba984e9 100644 --- a/src/Rules/Classes/ClassConstantRule.php +++ b/src/Rules/Classes/ClassConstantRule.php @@ -4,11 +4,14 @@ use PhpParser\Node; use PhpParser\Node\Expr\ClassConstFetch; +use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; @@ -17,32 +20,24 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; +use function array_merge; +use function in_array; +use function sprintf; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\ClassConstFetch> + * @implements Rule */ -class ClassConstantRule implements \PHPStan\Rules\Rule +class ClassConstantRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private PhpVersion $phpVersion; - public function __construct( - ReflectionProvider $reflectionProvider, - RuleLevelHelper $ruleLevelHelper, - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - PhpVersion $phpVersion + private ReflectionProvider $reflectionProvider, + private RuleLevelHelper $ruleLevelHelper, + private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private PhpVersion $phpVersion, ) { - $this->reflectionProvider = $reflectionProvider; - $this->ruleLevelHelper = $ruleLevelHelper; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->phpVersion = $phpVersion; } public function getNodeType(): string @@ -59,7 +54,7 @@ public function processNode(Node $node, Scope $scope): array $class = $node->class; $messages = []; - if ($class instanceof \PhpParser\Node\Name) { + if ($class instanceof Node\Name) { $className = (string) $class; $lowercasedClassName = strtolower($className); if (in_array($lowercasedClassName, ['self', 'static'], true)) { @@ -77,12 +72,12 @@ public function processNode(Node $node, Scope $scope): array ]; } $currentClassReflection = $scope->getClassReflection(); - if ($currentClassReflection->getParentClass() === false) { + if ($currentClassReflection->getParentClass() === null) { return [ RuleErrorBuilder::message(sprintf( 'Access to parent::%s but %s does not extend any class.', $constantName, - $currentClassReflection->getDisplayName() + $currentClassReflection->getDisplayName(), ))->build(), ]; } @@ -101,7 +96,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message( - sprintf('Access to constant %s on an unknown class %s.', $constantName, $className) + sprintf('Access to constant %s on an unknown class %s.', $constantName, $className), )->discoveringSymbolsTip()->build(), ]; } else { @@ -117,11 +112,9 @@ public function processNode(Node $node, Scope $scope): array } else { $classTypeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, - $class, - sprintf('Access to constant %s on an unknown class %%s.', $constantName), - static function (Type $type) use ($constantName): bool { - return $type->canAccessConstants()->yes() && $type->hasConstant($constantName)->yes(); - } + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $class), + sprintf('Access to constant %s on an unknown class %%s.', SprintfHelper::escapeFormatString($constantName)), + static fn (Type $type): bool => $type->canAccessConstants()->yes() && $type->hasConstant($constantName)->yes(), ); $classType = $classTypeResult->getType(); if ($classType instanceof ErrorType) { @@ -162,7 +155,7 @@ static function (Type $type) use ($constantName): bool { RuleErrorBuilder::message(sprintf( 'Cannot access constant %s on %s.', $constantName, - $typeForDescribe->describe(VerbosityLevel::typeOnly()) + $typeForDescribe->describe(VerbosityLevel::typeOnly()), ))->build(), ]); } @@ -176,7 +169,7 @@ static function (Type $type) use ($constantName): bool { RuleErrorBuilder::message(sprintf( 'Access to undefined constant %s::%s.', $typeForDescribe->describe(VerbosityLevel::typeOnly()), - $constantName + $constantName, ))->build(), ]); } @@ -188,7 +181,7 @@ static function (Type $type) use ($constantName): bool { 'Access to %s constant %s of class %s.', $constantReflection->isPrivate() ? 'private' : 'protected', $constantName, - $constantReflection->getDeclaringClass()->getDisplayName() + $constantReflection->getDeclaringClass()->getDisplayName(), ))->build(), ]); } diff --git a/src/Rules/Classes/DuplicateDeclarationRule.php b/src/Rules/Classes/DuplicateDeclarationRule.php index eaee0ead9f..257bf2eaf7 100644 --- a/src/Rules/Classes/DuplicateDeclarationRule.php +++ b/src/Rules/Classes/DuplicateDeclarationRule.php @@ -3,17 +3,22 @@ namespace PHPStan\Rules\Classes; use PhpParser\Node; +use PhpParser\Node\Stmt\ClassConst; +use PhpParser\Node\Stmt\EnumCase; use PHPStan\Analyser\Scope; use PHPStan\Node\InClassNode; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; use function array_key_exists; +use function is_string; use function sprintf; use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\InClassNode> + * @implements Rule */ -class DuplicateDeclarationRule implements \PHPStan\Rules\Rule +class DuplicateDeclarationRule implements Rule { public function getNodeType(): string @@ -25,22 +30,34 @@ public function processNode(Node $node, Scope $scope): array { $classReflection = $scope->getClassReflection(); if ($classReflection === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $errors = []; - $declaredClassConstants = []; - foreach ($node->getOriginalNode()->getConstants() as $constDecl) { - foreach ($constDecl->consts as $const) { - if (array_key_exists($const->name->name, $declaredClassConstants)) { + $declaredClassConstantsOrEnumCases = []; + foreach ($node->getOriginalNode()->stmts as $stmtNode) { + if ($stmtNode instanceof EnumCase) { + if (array_key_exists($stmtNode->name->name, $declaredClassConstantsOrEnumCases)) { $errors[] = RuleErrorBuilder::message(sprintf( - 'Cannot redeclare constant %s::%s.', + 'Cannot redeclare enum case %s::%s.', $classReflection->getDisplayName(), - $const->name->name - ))->line($const->getLine())->nonIgnorable()->build(); + $stmtNode->name->name, + ))->line($stmtNode->getLine())->nonIgnorable()->build(); } else { - $declaredClassConstants[$const->name->name] = true; + $declaredClassConstantsOrEnumCases[$stmtNode->name->name] = true; + } + } elseif ($stmtNode instanceof ClassConst) { + foreach ($stmtNode->consts as $classConstNode) { + if (array_key_exists($classConstNode->name->name, $declaredClassConstantsOrEnumCases)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Cannot redeclare constant %s::%s.', + $classReflection->getDisplayName(), + $classConstNode->name->name, + ))->line($classConstNode->getLine())->nonIgnorable()->build(); + } else { + $declaredClassConstantsOrEnumCases[$classConstNode->name->name] = true; + } } } } @@ -52,7 +69,7 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( 'Cannot redeclare property %s::$%s.', $classReflection->getDisplayName(), - $property->name->name + $property->name->name, ))->line($property->getLine())->nonIgnorable()->build(); } else { $declaredProperties[$property->name->name] = true; @@ -69,7 +86,7 @@ public function processNode(Node $node, Scope $scope): array } if (!$param->var instanceof Node\Expr\Variable || !is_string($param->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $propertyName = $param->var->name; @@ -78,7 +95,7 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( 'Cannot redeclare property %s::$%s.', $classReflection->getDisplayName(), - $propertyName + $propertyName, ))->line($param->getLine())->nonIgnorable()->build(); } else { $declaredProperties[$propertyName] = true; @@ -89,7 +106,7 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( 'Cannot redeclare method %s::%s().', $classReflection->getDisplayName(), - $method->name->name + $method->name->name, ))->line($method->getStartLine())->nonIgnorable()->build(); } else { $declaredFunctions[strtolower($method->name->name)] = true; diff --git a/src/Rules/Classes/EnumSanityRule.php b/src/Rules/Classes/EnumSanityRule.php new file mode 100644 index 0000000000..af3de8df85 --- /dev/null +++ b/src/Rules/Classes/EnumSanityRule.php @@ -0,0 +1,109 @@ + + */ +class EnumSanityRule implements Rule +{ + + private const ALLOWED_MAGIC_METHODS = [ + '__call' => true, + '__callstatic' => true, + '__invoke' => true, + ]; + + public function getNodeType(): string + { + return Node\Stmt\Enum_::class; + } + + /** + * @param Node\Stmt\Enum_ $node + */ + public function processNode(Node $node, Scope $scope): array + { + $errors = []; + + if ($node->namespacedName === null) { + throw new ShouldNotHappenException(); + } + + foreach ($node->getMethods() as $methodNode) { + if ($methodNode->isAbstract()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Enum %s contains abstract method %s().', + $node->namespacedName->toString(), + $methodNode->name->name, + ))->line($methodNode->getLine())->nonIgnorable()->build(); + } + + $lowercasedMethodName = $methodNode->name->toLowerString(); + + if ($methodNode->isMagic()) { + if ($lowercasedMethodName === '__construct') { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Enum %s contains constructor.', + $node->namespacedName->toString(), + ))->line($methodNode->getLine())->nonIgnorable()->build(); + } elseif ($lowercasedMethodName === '__destruct') { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Enum %s contains destructor.', + $node->namespacedName->toString(), + ))->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(), + $methodNode->name->name, + ))->line($methodNode->getLine())->nonIgnorable()->build(); + } + } + + if ($lowercasedMethodName === 'cases') { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Enum %s cannot redeclare native method %s().', + $node->namespacedName->toString(), + $methodNode->name->name, + ))->line($methodNode->getLine())->nonIgnorable()->build(); + } + + if ($node->scalarType === null) { + continue; + } + + if ($lowercasedMethodName !== 'from' && $lowercasedMethodName !== 'tryfrom') { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Enum %s cannot redeclare native method %s().', + $node->namespacedName->toString(), + $methodNode->name->name, + ))->line($methodNode->getLine())->nonIgnorable()->build(); + } + + if ( + $node->scalarType !== null + && $node->scalarType->name !== 'int' + && $node->scalarType->name !== 'string' + ) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Backed enum %s can have only "int" or "string" type.', + $node->namespacedName->toString(), + ))->line($node->scalarType->getLine())->nonIgnorable()->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Classes/ExistingClassInClassExtendsRule.php b/src/Rules/Classes/ExistingClassInClassExtendsRule.php index 53e049e9d1..513c7efcd5 100644 --- a/src/Rules/Classes/ExistingClassInClassExtendsRule.php +++ b/src/Rules/Classes/ExistingClassInClassExtendsRule.php @@ -7,25 +7,21 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Class_> + * @implements Rule */ -class ExistingClassInClassExtendsRule implements \PHPStan\Rules\Rule +class ExistingClassInClassExtendsRule implements Rule { - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private ReflectionProvider $reflectionProvider; - public function __construct( - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - ReflectionProvider $reflectionProvider + private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ReflectionProvider $reflectionProvider, ) { - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -49,7 +45,7 @@ public function processNode(Node $node, Scope $scope): array $messages[] = RuleErrorBuilder::message(sprintf( '%s extends unknown class %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', - $extendedClassName + $extendedClassName, ))->nonIgnorable()->discoveringSymbolsTip()->build(); } } else { @@ -58,20 +54,32 @@ public function processNode(Node $node, Scope $scope): array $messages[] = RuleErrorBuilder::message(sprintf( '%s extends interface %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', - $extendedClassName + $reflection->getDisplayName(), ))->nonIgnorable()->build(); } elseif ($reflection->isTrait()) { $messages[] = RuleErrorBuilder::message(sprintf( '%s extends trait %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', - $extendedClassName + $reflection->getDisplayName(), + ))->nonIgnorable()->build(); + } elseif ($reflection->isEnum()) { + $messages[] = RuleErrorBuilder::message(sprintf( + '%s extends enum %s.', + $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', + $reflection->getDisplayName(), ))->nonIgnorable()->build(); } elseif ($reflection->isFinalByKeyword()) { $messages[] = RuleErrorBuilder::message(sprintf( '%s extends final class %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', - $extendedClassName + $reflection->getDisplayName(), ))->nonIgnorable()->build(); + } elseif ($reflection->isFinal()) { + $messages[] = RuleErrorBuilder::message(sprintf( + '%s extends @final class %s.', + $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', + $reflection->getDisplayName(), + ))->build(); } } diff --git a/src/Rules/Classes/ExistingClassInInstanceOfRule.php b/src/Rules/Classes/ExistingClassInInstanceOfRule.php index c0b83b1e4b..775eadf450 100644 --- a/src/Rules/Classes/ExistingClassInInstanceOfRule.php +++ b/src/Rules/Classes/ExistingClassInInstanceOfRule.php @@ -8,29 +8,24 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function in_array; +use function sprintf; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Instanceof_> + * @implements Rule */ -class ExistingClassInInstanceOfRule implements \PHPStan\Rules\Rule +class ExistingClassInInstanceOfRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private bool $checkClassCaseSensitivity; - public function __construct( - ReflectionProvider $reflectionProvider, - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - bool $checkClassCaseSensitivity + private ReflectionProvider $reflectionProvider, + private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private bool $checkClassCaseSensitivity, ) { - $this->reflectionProvider = $reflectionProvider; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->checkClassCaseSensitivity = $checkClassCaseSensitivity; } public function getNodeType(): string @@ -41,7 +36,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $class = $node->class; - if (!($class instanceof \PhpParser\Node\Name)) { + if (!($class instanceof Node\Name)) { return []; } diff --git a/src/Rules/Classes/ExistingClassInTraitUseRule.php b/src/Rules/Classes/ExistingClassInTraitUseRule.php index a26b0b6387..43e84d80aa 100644 --- a/src/Rules/Classes/ExistingClassInTraitUseRule.php +++ b/src/Rules/Classes/ExistingClassInTraitUseRule.php @@ -7,42 +7,38 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; +use function array_map; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\TraitUse> + * @implements Rule */ -class ExistingClassInTraitUseRule implements \PHPStan\Rules\Rule +class ExistingClassInTraitUseRule implements Rule { - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private ReflectionProvider $reflectionProvider; - public function __construct( - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - ReflectionProvider $reflectionProvider + private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ReflectionProvider $reflectionProvider, ) { - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string { - return \PhpParser\Node\Stmt\TraitUse::class; + return Node\Stmt\TraitUse::class; } public function processNode(Node $node, Scope $scope): array { $messages = $this->classCaseSensitivityCheck->checkClassNames( - array_map(static function (Node\Name $traitName): ClassNameNodePair { - return new ClassNameNodePair((string) $traitName, $traitName); - }, $node->traits) + array_map(static fn (Node\Name $traitName): ClassNameNodePair => new ClassNameNodePair((string) $traitName, $traitName), $node->traits), ); if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $classReflection = $scope->getClassReflection(); @@ -69,9 +65,11 @@ public function processNode(Node $node, Scope $scope): array } else { $reflection = $this->reflectionProvider->getClass($traitName); if ($reflection->isClass()) { - $messages[] = RuleErrorBuilder::message(sprintf('%s uses class %s.', $currentName, $traitName))->nonIgnorable()->build(); + $messages[] = RuleErrorBuilder::message(sprintf('%s uses class %s.', $currentName, $reflection->getDisplayName()))->nonIgnorable()->build(); } elseif ($reflection->isInterface()) { - $messages[] = RuleErrorBuilder::message(sprintf('%s uses interface %s.', $currentName, $traitName))->nonIgnorable()->build(); + $messages[] = RuleErrorBuilder::message(sprintf('%s uses interface %s.', $currentName, $reflection->getDisplayName()))->nonIgnorable()->build(); + } elseif ($reflection->isEnum()) { + $messages[] = RuleErrorBuilder::message(sprintf('%s uses enum %s.', $currentName, $reflection->getDisplayName()))->nonIgnorable()->build(); } } } diff --git a/src/Rules/Classes/ExistingClassesInClassImplementsRule.php b/src/Rules/Classes/ExistingClassesInClassImplementsRule.php index 5e3bef9fb3..fde6d21979 100644 --- a/src/Rules/Classes/ExistingClassesInClassImplementsRule.php +++ b/src/Rules/Classes/ExistingClassesInClassImplementsRule.php @@ -7,25 +7,22 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function array_map; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Class_> + * @implements Rule */ -class ExistingClassesInClassImplementsRule implements \PHPStan\Rules\Rule +class ExistingClassesInClassImplementsRule implements Rule { - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private ReflectionProvider $reflectionProvider; - public function __construct( - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - ReflectionProvider $reflectionProvider + private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ReflectionProvider $reflectionProvider, ) { - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -36,9 +33,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $messages = $this->classCaseSensitivityCheck->checkClassNames( - array_map(static function (Node\Name $interfaceName): ClassNameNodePair { - return new ClassNameNodePair((string) $interfaceName, $interfaceName); - }, $node->implements) + array_map(static fn (Node\Name $interfaceName): ClassNameNodePair => new ClassNameNodePair((string) $interfaceName, $interfaceName), $node->implements), ); $currentClassName = null; @@ -53,7 +48,7 @@ public function processNode(Node $node, Scope $scope): array $messages[] = RuleErrorBuilder::message(sprintf( '%s implements unknown interface %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', - $implementedClassName + $implementedClassName, ))->nonIgnorable()->discoveringSymbolsTip()->build(); } } else { @@ -62,13 +57,19 @@ public function processNode(Node $node, Scope $scope): array $messages[] = RuleErrorBuilder::message(sprintf( '%s implements class %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', - $implementedClassName + $reflection->getDisplayName(), ))->nonIgnorable()->build(); } elseif ($reflection->isTrait()) { $messages[] = RuleErrorBuilder::message(sprintf( '%s implements trait %s.', $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', - $implementedClassName + $reflection->getDisplayName(), + ))->nonIgnorable()->build(); + } elseif ($reflection->isEnum()) { + $messages[] = RuleErrorBuilder::message(sprintf( + '%s implements enum %s.', + $currentClassName !== null ? sprintf('Class %s', $currentClassName) : 'Anonymous class', + $reflection->getDisplayName(), ))->nonIgnorable()->build(); } } diff --git a/src/Rules/Classes/ExistingClassesInEnumImplementsRule.php b/src/Rules/Classes/ExistingClassesInEnumImplementsRule.php new file mode 100644 index 0000000000..81f6168eba --- /dev/null +++ b/src/Rules/Classes/ExistingClassesInEnumImplementsRule.php @@ -0,0 +1,78 @@ + + */ +class ExistingClassesInEnumImplementsRule implements Rule +{ + + public function __construct( + private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ReflectionProvider $reflectionProvider, + ) + { + } + + public function getNodeType(): string + { + return Node\Stmt\Enum_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $messages = $this->classCaseSensitivityCheck->checkClassNames( + array_map(static fn (Node\Name $interfaceName): ClassNameNodePair => new ClassNameNodePair((string) $interfaceName, $interfaceName), $node->implements), + ); + + $currentEnumName = (string) $node->namespacedName; + + foreach ($node->implements as $implements) { + $implementedClassName = (string) $implements; + if (!$this->reflectionProvider->hasClass($implementedClassName)) { + if (!$scope->isInClassExists($implementedClassName)) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Enum %s implements unknown interface %s.', + $currentEnumName, + $implementedClassName, + ))->nonIgnorable()->discoveringSymbolsTip()->build(); + } + } else { + $reflection = $this->reflectionProvider->getClass($implementedClassName); + if ($reflection->isClass()) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Enum %s implements class %s.', + $currentEnumName, + $reflection->getDisplayName(), + ))->nonIgnorable()->build(); + } elseif ($reflection->isTrait()) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Enum %s implements trait %s.', + $currentEnumName, + $reflection->getDisplayName(), + ))->nonIgnorable()->build(); + } elseif ($reflection->isEnum()) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Enum %s implements enum %s.', + $currentEnumName, + $reflection->getDisplayName(), + ))->nonIgnorable()->build(); + } + } + } + + return $messages; + } + +} diff --git a/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php b/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php index 0dfc51bf64..f267550104 100644 --- a/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php +++ b/src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php @@ -7,25 +7,22 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function array_map; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Interface_> + * @implements Rule */ -class ExistingClassesInInterfaceExtendsRule implements \PHPStan\Rules\Rule +class ExistingClassesInInterfaceExtendsRule implements Rule { - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private ReflectionProvider $reflectionProvider; - public function __construct( - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - ReflectionProvider $reflectionProvider + private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private ReflectionProvider $reflectionProvider, ) { - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -36,9 +33,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { $messages = $this->classCaseSensitivityCheck->checkClassNames( - array_map(static function (Node\Name $interfaceName): ClassNameNodePair { - return new ClassNameNodePair((string) $interfaceName, $interfaceName); - }, $node->extends) + array_map(static fn (Node\Name $interfaceName): ClassNameNodePair => new ClassNameNodePair((string) $interfaceName, $interfaceName), $node->extends), ); $currentInterfaceName = (string) $node->namespacedName; @@ -49,7 +44,7 @@ public function processNode(Node $node, Scope $scope): array $messages[] = RuleErrorBuilder::message(sprintf( 'Interface %s extends unknown interface %s.', $currentInterfaceName, - $extendedInterfaceName + $extendedInterfaceName, ))->nonIgnorable()->discoveringSymbolsTip()->build(); } } else { @@ -58,13 +53,19 @@ public function processNode(Node $node, Scope $scope): array $messages[] = RuleErrorBuilder::message(sprintf( 'Interface %s extends class %s.', $currentInterfaceName, - $extendedInterfaceName + $reflection->getDisplayName(), ))->nonIgnorable()->build(); } elseif ($reflection->isTrait()) { $messages[] = RuleErrorBuilder::message(sprintf( 'Interface %s extends trait %s.', $currentInterfaceName, - $extendedInterfaceName + $reflection->getDisplayName(), + ))->nonIgnorable()->build(); + } elseif ($reflection->isEnum()) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Interface %s extends enum %s.', + $currentInterfaceName, + $reflection->getDisplayName(), ))->nonIgnorable()->build(); } } diff --git a/src/Rules/Classes/ImpossibleInstanceOfRule.php b/src/Rules/Classes/ImpossibleInstanceOfRule.php index 02e3e4c63f..d047d4b2f5 100644 --- a/src/Rules/Classes/ImpossibleInstanceOfRule.php +++ b/src/Rules/Classes/ImpossibleInstanceOfRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\ObjectType; @@ -11,24 +12,19 @@ use PHPStan\Type\StringType; use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Instanceof_> + * @implements Rule */ -class ImpossibleInstanceOfRule implements \PHPStan\Rules\Rule +class ImpossibleInstanceOfRule implements Rule { - private bool $checkAlwaysTrueInstanceof; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - bool $checkAlwaysTrueInstanceof, - bool $treatPhpDocTypesAsCertain + private bool $checkAlwaysTrueInstanceof, + private bool $treatPhpDocTypesAsCertain, ) { - $this->checkAlwaysTrueInstanceof = $checkAlwaysTrueInstanceof; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function getNodeType(): string @@ -48,14 +44,14 @@ public function processNode(Node $node, Scope $scope): array $classType = $scope->getType($node->class); $allowed = TypeCombinator::union( new StringType(), - new ObjectWithoutClassType() + new ObjectWithoutClassType(), ); if (!$allowed->accepts($classType, true)->yes()) { return [ RuleErrorBuilder::message(sprintf( 'Instanceof between %s and %s results in an error.', $expressionType->describe(VerbosityLevel::typeOnly()), - $classType->describe(VerbosityLevel::typeOnly()) + $classType->describe(VerbosityLevel::typeOnly()), ))->build(), ]; } @@ -83,7 +79,7 @@ public function processNode(Node $node, Scope $scope): array $addTip(RuleErrorBuilder::message(sprintf( 'Instanceof between %s and %s will always evaluate to false.', $expressionType->describe(VerbosityLevel::typeOnly()), - $classType->describe(VerbosityLevel::typeOnly()) + $classType->describe(VerbosityLevel::typeOnly()), )))->build(), ]; } elseif ($this->checkAlwaysTrueInstanceof) { @@ -91,7 +87,7 @@ public function processNode(Node $node, Scope $scope): array $addTip(RuleErrorBuilder::message(sprintf( 'Instanceof between %s and %s will always evaluate to true.', $expressionType->describe(VerbosityLevel::typeOnly()), - $classType->describe(VerbosityLevel::typeOnly()) + $classType->describe(VerbosityLevel::typeOnly()), )))->build(), ]; } diff --git a/src/Rules/Classes/InstantiationCallableRule.php b/src/Rules/Classes/InstantiationCallableRule.php new file mode 100644 index 0000000000..ed9eae3be8 --- /dev/null +++ b/src/Rules/Classes/InstantiationCallableRule.php @@ -0,0 +1,29 @@ + + */ +class InstantiationCallableRule implements Rule +{ + + public function getNodeType(): string + { + return InstantiationCallableNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return [ + RuleErrorBuilder::message('Cannot create callable from the new operator.')->nonIgnorable()->build(), + ]; + } + +} diff --git a/src/Rules/Classes/InstantiationRule.php b/src/Rules/Classes/InstantiationRule.php index 9aa1612b21..2200f7772d 100644 --- a/src/Rules/Classes/InstantiationRule.php +++ b/src/Rules/Classes/InstantiationRule.php @@ -5,39 +5,38 @@ use PhpParser\Node; use PhpParser\Node\Expr\New_; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\Php\PhpMethodReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\FunctionCallParametersCheck; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\TypeUtils; use PHPStan\Type\TypeWithClassName; +use function array_map; +use function array_merge; +use function count; +use function sprintf; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\New_> + * @implements Rule */ -class InstantiationRule implements \PHPStan\Rules\Rule +class InstantiationRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\FunctionCallParametersCheck $check; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - public function __construct( - ReflectionProvider $reflectionProvider, - FunctionCallParametersCheck $check, - ClassCaseSensitivityCheck $classCaseSensitivityCheck + private ReflectionProvider $reflectionProvider, + private FunctionCallParametersCheck $check, + private ClassCaseSensitivityCheck $classCaseSensitivityCheck, ) { - $this->reflectionProvider = $reflectionProvider; - $this->check = $check; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; } public function getNodeType(): string @@ -55,9 +54,7 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param string $class - * @param \PhpParser\Node\Expr\New_ $node - * @param Scope $scope + * @param Node\Expr\New_ $node * @return RuleError[] */ private function checkClassName(string $class, bool $isName, Node $node, Scope $scope): array @@ -102,13 +99,13 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ RuleErrorBuilder::message(sprintf('Using %s outside of class scope.', $class))->build(), ]; } - if ($scope->getClassReflection()->getParentClass() === false) { + if ($scope->getClassReflection()->getParentClass() === null) { return [ RuleErrorBuilder::message(sprintf( '%s::%s() calls new parent but %s does not extend any class.', $scope->getClassReflection()->getDisplayName(), $scope->getFunctionName(), - $scope->getClassReflection()->getDisplayName() + $scope->getClassReflection()->getDisplayName(), ))->build(), ]; } @@ -131,10 +128,18 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ $classReflection = $this->reflectionProvider->getClass($class); } + if ($classReflection->isEnum() && $isName) { + return [ + RuleErrorBuilder::message( + sprintf('Cannot instantiate enum %s.', $classReflection->getDisplayName()), + )->build(), + ]; + } + if (!$isStatic && $classReflection->isInterface() && $isName) { return [ RuleErrorBuilder::message( - sprintf('Cannot instantiate interface %s.', $classReflection->getDisplayName()) + sprintf('Cannot instantiate interface %s.', $classReflection->getDisplayName()), )->build(), ]; } @@ -142,7 +147,7 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ if (!$isStatic && $classReflection->isAbstract() && $isName) { return [ RuleErrorBuilder::message( - sprintf('Instantiated class %s is abstract.', $classReflection->getDisplayName()) + sprintf('Instantiated class %s is abstract.', $classReflection->getDisplayName()), )->build(), ]; } @@ -152,11 +157,11 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ } if (!$classReflection->hasConstructor()) { - if (count($node->args) > 0) { + if (count($node->getArgs()) > 0) { return array_merge($messages, [ RuleErrorBuilder::message(sprintf( 'Class %s does not have a constructor and must be instantiated without any parameters.', - $classReflection->getDisplayName() + $classReflection->getDisplayName(), ))->build(), ]); } @@ -171,52 +176,53 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ $classReflection->getDisplayName(), $constructorReflection->isPrivate() ? 'private' : 'protected', $constructorReflection->getDeclaringClass()->getDisplayName(), - $constructorReflection->getName() + $constructorReflection->getName(), ))->build(); } + $classDisplayName = SprintfHelper::escapeFormatString($classReflection->getDisplayName()); + return array_merge($messages, $this->check->check( ParametersAcceptorSelector::selectFromArgs( $scope, - $node->args, - $constructorReflection->getVariants() + $node->getArgs(), + $constructorReflection->getVariants(), ), $scope, $constructorReflection->getDeclaringClass()->isBuiltin(), $node, [ - 'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameter, %d required.', - 'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameters, %d required.', - 'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameter, at least %d required.', - 'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameters, at least %d required.', - 'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameter, %d-%d required.', - 'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameters, %d-%d required.', - 'Parameter %s of class ' . $classReflection->getDisplayName() . ' constructor expects %s, %s given.', + 'Class ' . $classDisplayName . ' constructor invoked with %d parameter, %d required.', + 'Class ' . $classDisplayName . ' constructor invoked with %d parameters, %d required.', + 'Class ' . $classDisplayName . ' constructor invoked with %d parameter, at least %d required.', + 'Class ' . $classDisplayName . ' constructor invoked with %d parameters, at least %d required.', + 'Class ' . $classDisplayName . ' constructor invoked with %d parameter, %d-%d required.', + 'Class ' . $classDisplayName . ' constructor invoked with %d parameters, %d-%d required.', + 'Parameter %s of class ' . $classDisplayName . ' constructor expects %s, %s given.', '', // constructor does not have a return type - 'Parameter %s of class ' . $classReflection->getDisplayName() . ' constructor is passed by reference, so it expects variables only', - 'Unable to resolve the template type %s in instantiation of class ' . $classReflection->getDisplayName(), - 'Missing parameter $%s in call to ' . $classReflection->getDisplayName() . ' constructor.', - 'Unknown parameter $%s in call to ' . $classReflection->getDisplayName() . ' constructor.', - 'Return type of call to ' . $classReflection->getDisplayName() . ' constructor contains unresolvable type.', - ] + 'Parameter %s of class ' . $classDisplayName . ' constructor is passed by reference, so it expects variables only', + 'Unable to resolve the template type %s in instantiation of class ' . $classDisplayName, + 'Missing parameter $%s in call to ' . $classDisplayName . ' constructor.', + 'Unknown parameter $%s in call to ' . $classDisplayName . ' constructor.', + 'Return type of call to ' . $classDisplayName . ' constructor contains unresolvable type.', + ], )); } /** - * @param \PhpParser\Node\Expr\New_ $node $node - * @param Scope $scope + * @param Node\Expr\New_ $node $node * @return array */ private function getClassNames(Node $node, Scope $scope): array { - if ($node->class instanceof \PhpParser\Node\Name) { + if ($node->class instanceof Node\Name) { return [[(string) $node->class, true]]; } if ($node->class instanceof Node\Stmt\Class_) { $anonymousClassType = $scope->getType($node); if (!$anonymousClassType instanceof TypeWithClassName) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return [[$anonymousClassType->getClassName(), true]]; @@ -226,17 +232,13 @@ private function getClassNames(Node $node, Scope $scope): array return array_merge( array_map( - static function (ConstantStringType $type): array { - return [$type->getValue(), true]; - }, - TypeUtils::getConstantStrings($type) + static fn (ConstantStringType $type): array => [$type->getValue(), true], + TypeUtils::getConstantStrings($type), ), array_map( - static function (string $name): array { - return [$name, false]; - }, - TypeUtils::getDirectClassNames($type) - ) + static fn (string $name): array => [$name, false], + TypeUtils::getDirectClassNames($type), + ), ); } diff --git a/src/Rules/Classes/InvalidPromotedPropertiesRule.php b/src/Rules/Classes/InvalidPromotedPropertiesRule.php index 0e57df511f..b81e387b60 100644 --- a/src/Rules/Classes/InvalidPromotedPropertiesRule.php +++ b/src/Rules/Classes/InvalidPromotedPropertiesRule.php @@ -7,6 +7,9 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; +use function is_string; +use function sprintf; /** * @implements Rule @@ -14,11 +17,8 @@ class InvalidPromotedPropertiesRule implements Rule { - private PhpVersion $phpVersion; - - public function __construct(PhpVersion $phpVersion) + public function __construct(private PhpVersion $phpVersion) { - $this->phpVersion = $phpVersion; } public function getNodeType(): string @@ -54,7 +54,7 @@ public function processNode(Node $node, Scope $scope): array if (!$this->phpVersion->supportsPromotedProperties()) { return [ RuleErrorBuilder::message( - 'Promoted properties are supported only on PHP 8.0 and later.' + 'Promoted properties are supported only on PHP 8.0 and later.', )->nonIgnorable()->build(), ]; } @@ -65,7 +65,7 @@ public function processNode(Node $node, Scope $scope): array ) { return [ RuleErrorBuilder::message( - 'Promoted properties can be in constructor only.' + 'Promoted properties can be in constructor only.', )->nonIgnorable()->build(), ]; } @@ -73,7 +73,7 @@ public function processNode(Node $node, Scope $scope): array if ($node->stmts === null) { return [ RuleErrorBuilder::message( - 'Promoted properties are not allowed in abstract constructors.' + 'Promoted properties are not allowed in abstract constructors.', )->nonIgnorable()->build(), ]; } @@ -85,7 +85,7 @@ public function processNode(Node $node, Scope $scope): array } if (!$param->var instanceof Node\Expr\Variable || !is_string($param->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if (!$param->variadic) { @@ -94,7 +94,7 @@ public function processNode(Node $node, Scope $scope): array $propertyName = $param->var->name; $errors[] = RuleErrorBuilder::message( - sprintf('Promoted property parameter $%s can not be variadic.', $propertyName) + sprintf('Promoted property parameter $%s can not be variadic.', $propertyName), )->nonIgnorable()->line($param->getLine())->build(); continue; } diff --git a/src/Rules/Classes/LocalTypeAliasesRule.php b/src/Rules/Classes/LocalTypeAliasesRule.php index 9773b79dc6..cdc76cdb61 100644 --- a/src/Rules/Classes/LocalTypeAliasesRule.php +++ b/src/Rules/Classes/LocalTypeAliasesRule.php @@ -11,10 +11,15 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Type\CircularTypeAliasErrorType; use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\ObjectType; +use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; +use function array_key_exists; +use function in_array; +use function sprintf; /** * @implements Rule @@ -22,25 +27,15 @@ class LocalTypeAliasesRule implements Rule { - /** @var array */ - private array $globalTypeAliases; - - private ReflectionProvider $reflectionProvider; - - private TypeNodeResolver $typeNodeResolver; - /** * @param array $globalTypeAliases */ public function __construct( - array $globalTypeAliases, - ReflectionProvider $reflectionProvider, - TypeNodeResolver $typeNodeResolver + private array $globalTypeAliases, + private ReflectionProvider $reflectionProvider, + private TypeNodeResolver $typeNodeResolver, ) { - $this->globalTypeAliases = $globalTypeAliases; - $this->reflectionProvider = $reflectionProvider; - $this->typeNodeResolver = $typeNodeResolver; } public function getNodeType(): string @@ -88,8 +83,18 @@ public function processNode(Node $node, Scope $scope): array continue; } + $resolvedName = $resolveName($aliasName); if ($this->reflectionProvider->hasClass($resolveName($aliasName))) { - $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a class in scope of %s.', $aliasName, $className))->build(); + $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; } @@ -115,8 +120,18 @@ public function processNode(Node $node, Scope $scope): array continue; } - if ($this->reflectionProvider->hasClass($resolveName($aliasName))) { - $errors[] = RuleErrorBuilder::message(sprintf('Type alias %s already exists as a class in scope of %s.', $aliasName, $className))->build(); + $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; } @@ -132,17 +147,23 @@ public function processNode(Node $node, Scope $scope): array $resolvedType = $typeAliasTag->getTypeAlias()->resolve($this->typeNodeResolver); $foundError = false; - TypeTraverser::map($resolvedType, static function (\PHPStan\Type\Type $type, callable $traverse) use (&$errors, &$foundError, $aliasName): \PHPStan\Type\Type { + TypeTraverser::map($resolvedType, static function (Type $type, callable $traverse) use (&$errors, &$foundError, $aliasName): Type { if ($foundError) { return $type; } - if ($type instanceof ErrorType) { + 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); }); } diff --git a/src/Rules/Classes/MixinRule.php b/src/Rules/Classes/MixinRule.php index 1a4c6797c4..93d0f4bfc4 100644 --- a/src/Rules/Classes/MixinRule.php +++ b/src/Rules/Classes/MixinRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\InClassNode; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassNameNodePair; @@ -12,73 +13,40 @@ use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Type\FileTypeMapper; use PHPStan\Type\VerbosityLevel; +use function array_merge; +use function implode; +use function sprintf; /** - * @implements Rule + * @implements Rule */ class MixinRule implements Rule { - private FileTypeMapper $fileTypeMapper; - - private ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private \PHPStan\Rules\Generics\GenericObjectTypeCheck $genericObjectTypeCheck; - - private MissingTypehintCheck $missingTypehintCheck; - - private UnresolvableTypeHelper $unresolvableTypeHelper; - - private bool $checkClassCaseSensitivity; - public function __construct( - FileTypeMapper $fileTypeMapper, - ReflectionProvider $reflectionProvider, - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - GenericObjectTypeCheck $genericObjectTypeCheck, - MissingTypehintCheck $missingTypehintCheck, - UnresolvableTypeHelper $unresolvableTypeHelper, - bool $checkClassCaseSensitivity + private ReflectionProvider $reflectionProvider, + private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private GenericObjectTypeCheck $genericObjectTypeCheck, + private MissingTypehintCheck $missingTypehintCheck, + private UnresolvableTypeHelper $unresolvableTypeHelper, + private bool $checkClassCaseSensitivity, ) { - $this->fileTypeMapper = $fileTypeMapper; - $this->reflectionProvider = $reflectionProvider; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->genericObjectTypeCheck = $genericObjectTypeCheck; - $this->missingTypehintCheck = $missingTypehintCheck; - $this->unresolvableTypeHelper = $unresolvableTypeHelper; - $this->checkClassCaseSensitivity = $checkClassCaseSensitivity; } public function getNodeType(): string { - return Node\Stmt\Class_::class; + return InClassNode::class; } public function processNode(Node $node, Scope $scope): array { - if (!isset($node->namespacedName)) { - // anonymous class - return []; - } - - $className = (string) $node->namespacedName; - $docComment = $node->getDocComment(); - if ($docComment === null) { + if (!$scope->isInClass()) { return []; } - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $scope->getFile(), - $className, - null, - null, - $docComment->getText() - ); - $mixinTags = $resolvedPhpDoc->getMixinTags(); + $classReflection = $scope->getClassReflection(); + $mixinTags = $classReflection->getMixinTags(); $errors = []; foreach ($mixinTags as $mixinTag) { $type = $mixinTag->getType(); @@ -96,17 +64,17 @@ public function processNode(Node $node, Scope $scope): array $errors = array_merge($errors, $this->genericObjectTypeCheck->check( $type, - 'PHPDoc tag @mixin contains generic type %s but class %s is not generic.', - 'Generic type %s in PHPDoc tag @mixin does not specify all template types of class %s: %s', - 'Generic type %s in PHPDoc tag @mixin specifies %d template types, but class %s supports only %d: %s', - 'Type %s in generic type %s in PHPDoc tag @mixin is not subtype of template type %s of class %s.' + 'PHPDoc tag @mixin contains generic type %s but %s %s is not generic.', + '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.', )); foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($type) as [$innerName, $genericTypeNames]) { $errors[] = RuleErrorBuilder::message(sprintf( 'PHPDoc tag @mixin contains generic %s but does not specify its types: %s', $innerName, - implode(', ', $genericTypeNames) + implode(', ', $genericTypeNames), ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); } @@ -120,7 +88,7 @@ public function processNode(Node $node, Scope $scope): array $errors, $this->classCaseSensitivityCheck->checkClassNames([ new ClassNameNodePair($class, $node), - ]) + ]), ); } } diff --git a/src/Rules/Classes/NewStaticRule.php b/src/Rules/Classes/NewStaticRule.php index e93c93f68c..3c3ba04562 100644 --- a/src/Rules/Classes/NewStaticRule.php +++ b/src/Rules/Classes/NewStaticRule.php @@ -7,9 +7,10 @@ use PHPStan\Reflection\Php\PhpMethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\New_> + * @implements Rule */ class NewStaticRule implements Rule { diff --git a/src/Rules/Classes/NonClassAttributeClassRule.php b/src/Rules/Classes/NonClassAttributeClassRule.php index 3a8bcedbbf..0138692224 100644 --- a/src/Rules/Classes/NonClassAttributeClassRule.php +++ b/src/Rules/Classes/NonClassAttributeClassRule.php @@ -8,6 +8,8 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; +use function sprintf; /** * @implements Rule @@ -36,18 +38,20 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param Scope $scope * @return RuleError[] */ private function check(Scope $scope): array { if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $classReflection = $scope->getClassReflection(); if (!$classReflection->isClass()) { return [ - RuleErrorBuilder::message('Interface cannot be an Attribute class.')->build(), + RuleErrorBuilder::message(sprintf( + '%s cannot be an Attribute class.', + $classReflection->isInterface() ? 'Interface' : 'Enum', + ))->build(), ]; } if ($classReflection->isAbstract()) { diff --git a/src/Rules/Classes/UnusedConstructorParametersRule.php b/src/Rules/Classes/UnusedConstructorParametersRule.php index 1d27034d71..a6383e9f23 100644 --- a/src/Rules/Classes/UnusedConstructorParametersRule.php +++ b/src/Rules/Classes/UnusedConstructorParametersRule.php @@ -6,21 +6,28 @@ use PhpParser\Node\Expr\Variable; use PhpParser\Node\Param; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InClassMethodNode; use PHPStan\Reflection\MethodReflection; +use PHPStan\Rules\Rule; use PHPStan\Rules\UnusedFunctionParametersCheck; +use PHPStan\ShouldNotHappenException; +use function array_filter; +use function array_map; +use function array_values; +use function count; +use function is_string; +use function sprintf; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -class UnusedConstructorParametersRule implements \PHPStan\Rules\Rule +class UnusedConstructorParametersRule implements Rule { - private \PHPStan\Rules\UnusedFunctionParametersCheck $check; - - public function __construct(UnusedFunctionParametersCheck $check) + public function __construct(private UnusedFunctionParametersCheck $check) { - $this->check = $check; } public function getNodeType(): string @@ -31,7 +38,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $method = $scope->getFunction(); @@ -50,7 +57,7 @@ public function processNode(Node $node, Scope $scope): array $message = sprintf( 'Constructor of class %s has an unused parameter $%%s.', - $scope->getClassReflection()->getDisplayName() + SprintfHelper::escapeFormatString($scope->getClassReflection()->getDisplayName()), ); if ($scope->getClassReflection()->isAnonymous()) { $message = 'Constructor of an anonymous class has an unused parameter $%s.'; @@ -60,16 +67,14 @@ public function processNode(Node $node, Scope $scope): array $scope, array_map(static function (Param $parameter): string { if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $parameter->var->name; - }, array_values(array_filter($originalNode->params, static function (Param $parameter): bool { - return $parameter->flags === 0; - }))), + }, array_values(array_filter($originalNode->params, static fn (Param $parameter): bool => $parameter->flags === 0))), $originalNode->stmts, $message, 'constructor.unusedParameter', - [] + [], ); } diff --git a/src/Rules/Comparison/BooleanAndConstantConditionRule.php b/src/Rules/Comparison/BooleanAndConstantConditionRule.php index 9feac9d37c..1339155d9e 100644 --- a/src/Rules/Comparison/BooleanAndConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanAndConstantConditionRule.php @@ -2,33 +2,26 @@ namespace PHPStan\Rules\Comparison; -use PhpParser\Node\Expr\BinaryOp\BooleanAnd; -use PhpParser\Node\Expr\BinaryOp\LogicalAnd; +use PhpParser\Node; +use PHPStan\Analyser\Scope; use PHPStan\Node\BooleanAndNode; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; +use function count; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -class BooleanAndConstantConditionRule implements \PHPStan\Rules\Rule +class BooleanAndConstantConditionRule implements Rule { - private ConstantConditionRuleHelper $helper; - - private bool $treatPhpDocTypesAsCertain; - - private bool $checkLogicalAndConstantCondition; - public function __construct( - ConstantConditionRuleHelper $helper, - bool $treatPhpDocTypesAsCertain, - bool $checkLogicalAndConstantCondition + private ConstantConditionRuleHelper $helper, + private bool $treatPhpDocTypesAsCertain, ) { - $this->helper = $helper; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; - $this->checkLogicalAndConstantCondition = $checkLogicalAndConstantCondition; } public function getNodeType(): string @@ -37,18 +30,12 @@ public function getNodeType(): string } public function processNode( - \PhpParser\Node $node, - \PHPStan\Analyser\Scope $scope + Node $node, + Scope $scope, ): array { $errors = []; - - /** @var BooleanAnd|LogicalAnd $originalNode */ $originalNode = $node->getOriginalNode(); - if (!$originalNode instanceof BooleanAnd && !$this->checkLogicalAndConstantCondition) { - return []; - } - $leftType = $this->helper->getBooleanType($scope, $originalNode->left); $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; if ($leftType instanceof ConstantBooleanType) { @@ -66,14 +53,14 @@ public function processNode( }; $errors[] = $addTipLeft(RuleErrorBuilder::message(sprintf( 'Left side of && is always %s.', - $leftType->getValue() ? 'true' : 'false' + $leftType->getValue() ? 'true' : 'false', )))->line($originalNode->left->getLine())->build(); } $rightScope = $node->getRightScope(); $rightType = $this->helper->getBooleanType( $rightScope, - $originalNode->right + $originalNode->right, ); if ($rightType instanceof ConstantBooleanType) { $addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($rightScope, $originalNode, $tipText): RuleErrorBuilder { @@ -83,7 +70,7 @@ public function processNode( $booleanNativeType = $this->helper->getNativeBooleanType( $rightScope->doNotTreatPhpDocTypesAsCertain(), - $originalNode->right + $originalNode->right, ); if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; @@ -93,7 +80,7 @@ public function processNode( }; $errors[] = $addTipRight(RuleErrorBuilder::message(sprintf( 'Right side of && is always %s.', - $rightType->getValue() ? 'true' : 'false' + $rightType->getValue() ? 'true' : 'false', )))->line($originalNode->right->getLine())->build(); } @@ -115,7 +102,7 @@ public function processNode( $errors[] = $addTip(RuleErrorBuilder::message(sprintf( 'Result of && is always %s.', - $nodeType->getValue() ? 'true' : 'false' + $nodeType->getValue() ? 'true' : 'false', )))->build(); } } diff --git a/src/Rules/Comparison/BooleanNotConstantConditionRule.php b/src/Rules/Comparison/BooleanNotConstantConditionRule.php index f87f3d342b..9a8ca4ae24 100644 --- a/src/Rules/Comparison/BooleanNotConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanNotConstantConditionRule.php @@ -2,36 +2,34 @@ namespace PHPStan\Rules\Comparison; +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\BooleanNot> + * @implements Rule */ -class BooleanNotConstantConditionRule implements \PHPStan\Rules\Rule +class BooleanNotConstantConditionRule implements Rule { - private ConstantConditionRuleHelper $helper; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ConstantConditionRuleHelper $helper, - bool $treatPhpDocTypesAsCertain + private ConstantConditionRuleHelper $helper, + private bool $treatPhpDocTypesAsCertain, ) { - $this->helper = $helper; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function getNodeType(): string { - return \PhpParser\Node\Expr\BooleanNot::class; + return Node\Expr\BooleanNot::class; } public function processNode( - \PhpParser\Node $node, - \PHPStan\Analyser\Scope $scope + Node $node, + Scope $scope, ): array { $exprType = $this->helper->getBooleanType($scope, $node->expr); @@ -52,7 +50,7 @@ public function processNode( return [ $addTip(RuleErrorBuilder::message(sprintf( 'Negated boolean expression is always %s.', - $exprType->getValue() ? 'false' : 'true' + $exprType->getValue() ? 'false' : 'true', )))->line($node->expr->getLine())->build(), ]; } diff --git a/src/Rules/Comparison/BooleanOrConstantConditionRule.php b/src/Rules/Comparison/BooleanOrConstantConditionRule.php index 4b29a14d4d..4606b16ef2 100644 --- a/src/Rules/Comparison/BooleanOrConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanOrConstantConditionRule.php @@ -2,32 +2,26 @@ namespace PHPStan\Rules\Comparison; -use PhpParser\Node\Expr\BinaryOp\BooleanOr; +use PhpParser\Node; +use PHPStan\Analyser\Scope; use PHPStan\Node\BooleanOrNode; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; +use function count; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -class BooleanOrConstantConditionRule implements \PHPStan\Rules\Rule +class BooleanOrConstantConditionRule implements Rule { - private ConstantConditionRuleHelper $helper; - - private bool $treatPhpDocTypesAsCertain; - - private bool $checkLogicalOrConstantCondition; - public function __construct( - ConstantConditionRuleHelper $helper, - bool $treatPhpDocTypesAsCertain, - bool $checkLogicalOrConstantCondition + private ConstantConditionRuleHelper $helper, + private bool $treatPhpDocTypesAsCertain, ) { - $this->helper = $helper; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; - $this->checkLogicalOrConstantCondition = $checkLogicalOrConstantCondition; } public function getNodeType(): string @@ -36,15 +30,11 @@ public function getNodeType(): string } public function processNode( - \PhpParser\Node $node, - \PHPStan\Analyser\Scope $scope + Node $node, + Scope $scope, ): array { $originalNode = $node->getOriginalNode(); - if (!$originalNode instanceof BooleanOr && !$this->checkLogicalOrConstantCondition) { - return []; - } - $messages = []; $leftType = $this->helper->getBooleanType($scope, $originalNode->left); $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; @@ -63,14 +53,14 @@ public function processNode( }; $messages[] = $addTipLeft(RuleErrorBuilder::message(sprintf( 'Left side of || is always %s.', - $leftType->getValue() ? 'true' : 'false' + $leftType->getValue() ? 'true' : 'false', )))->line($originalNode->left->getLine())->build(); } $rightScope = $node->getRightScope(); $rightType = $this->helper->getBooleanType( $rightScope, - $originalNode->right + $originalNode->right, ); if ($rightType instanceof ConstantBooleanType) { $addTipRight = function (RuleErrorBuilder $ruleErrorBuilder) use ($rightScope, $originalNode, $tipText): RuleErrorBuilder { @@ -80,7 +70,7 @@ public function processNode( $booleanNativeType = $this->helper->getNativeBooleanType( $rightScope->doNotTreatPhpDocTypesAsCertain(), - $originalNode->right + $originalNode->right, ); if ($booleanNativeType instanceof ConstantBooleanType) { return $ruleErrorBuilder; @@ -90,7 +80,7 @@ public function processNode( }; $messages[] = $addTipRight(RuleErrorBuilder::message(sprintf( 'Right side of || is always %s.', - $rightType->getValue() ? 'true' : 'false' + $rightType->getValue() ? 'true' : 'false', )))->line($originalNode->right->getLine())->build(); } @@ -111,7 +101,7 @@ public function processNode( }; $messages[] = $addTip(RuleErrorBuilder::message(sprintf( 'Result of || is always %s.', - $nodeType->getValue() ? 'true' : 'false' + $nodeType->getValue() ? 'true' : 'false', )))->build(); } } diff --git a/src/Rules/Comparison/ConstantConditionRuleHelper.php b/src/Rules/Comparison/ConstantConditionRuleHelper.php index 18de49a6c7..fd22403af1 100644 --- a/src/Rules/Comparison/ConstantConditionRuleHelper.php +++ b/src/Rules/Comparison/ConstantConditionRuleHelper.php @@ -11,17 +11,11 @@ class ConstantConditionRuleHelper { - private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, - bool $treatPhpDocTypesAsCertain + private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, + private bool $treatPhpDocTypesAsCertain, ) { - $this->impossibleCheckTypeHelper = $impossibleCheckTypeHelper; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function shouldReportAlwaysTrueByDefault(Expr $expr): bool @@ -30,7 +24,8 @@ public function shouldReportAlwaysTrueByDefault(Expr $expr): bool || $expr instanceof Expr\BinaryOp\BooleanOr || $expr instanceof Expr\BinaryOp\BooleanAnd || $expr instanceof Expr\Ternary - || $expr instanceof Expr\Isset_; + || $expr instanceof Expr\Isset_ + || $expr instanceof Expr\Empty_; } public function shouldSkip(Scope $scope, Expr $expr): bool @@ -44,6 +39,7 @@ public function shouldSkip(Scope $scope, Expr $expr): bool || $expr instanceof Expr\BinaryOp\BooleanAnd || $expr instanceof Expr\Ternary || $expr instanceof Expr\Isset_ + || $expr instanceof Expr\Empty_ || $expr instanceof Expr\BinaryOp\Greater || $expr instanceof Expr\BinaryOp\GreaterOrEqual || $expr instanceof Expr\BinaryOp\Smaller diff --git a/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php b/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php new file mode 100644 index 0000000000..9683f4c608 --- /dev/null +++ b/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php @@ -0,0 +1,88 @@ + + */ +class DoWhileLoopConstantConditionRule implements Rule +{ + + public function __construct( + private ConstantConditionRuleHelper $helper, + private bool $treatPhpDocTypesAsCertain, + ) + { + } + + public function getNodeType(): string + { + return DoWhileLoopConditionNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $exprType = $this->helper->getBooleanType($scope, $node->getCond()); + if ($exprType instanceof ConstantBooleanType) { + if ($exprType->getValue()) { + foreach ($node->getExitPoints() as $exitPoint) { + $statement = $exitPoint->getStatement(); + if ($statement instanceof Break_) { + return []; + } + if (!$statement instanceof Continue_) { + return []; + } + if ($statement->num === null) { + continue; + } + if (!$statement->num instanceof LNumber) { + continue; + } + $value = $statement->num->value; + if ($value === 1) { + continue; + } + + if ($value > 1) { + return []; + } + } + } + + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { + if (!$this->treatPhpDocTypesAsCertain) { + return $ruleErrorBuilder; + } + + $booleanNativeType = $this->helper->getNativeBooleanType($scope, $node->getCond()); + if ($booleanNativeType instanceof ConstantBooleanType) { + return $ruleErrorBuilder; + } + + return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + }; + + return [ + $addTip(RuleErrorBuilder::message(sprintf( + 'Do-while loop condition is always %s.', + $exprType->getValue() ? 'true' : 'false', + )))->line($node->getCond()->getLine())->build(), + ]; + } + + return []; + } + +} diff --git a/src/Rules/Comparison/ElseIfConstantConditionRule.php b/src/Rules/Comparison/ElseIfConstantConditionRule.php index 734ccabf49..31df891540 100644 --- a/src/Rules/Comparison/ElseIfConstantConditionRule.php +++ b/src/Rules/Comparison/ElseIfConstantConditionRule.php @@ -2,36 +2,34 @@ namespace PHPStan\Rules\Comparison; +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\ElseIf_> + * @implements Rule */ -class ElseIfConstantConditionRule implements \PHPStan\Rules\Rule +class ElseIfConstantConditionRule implements Rule { - private ConstantConditionRuleHelper $helper; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ConstantConditionRuleHelper $helper, - bool $treatPhpDocTypesAsCertain + private ConstantConditionRuleHelper $helper, + private bool $treatPhpDocTypesAsCertain, ) { - $this->helper = $helper; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function getNodeType(): string { - return \PhpParser\Node\Stmt\ElseIf_::class; + return Node\Stmt\ElseIf_::class; } public function processNode( - \PhpParser\Node $node, - \PHPStan\Analyser\Scope $scope + Node $node, + Scope $scope, ): array { $exprType = $this->helper->getBooleanType($scope, $node->cond); @@ -51,7 +49,7 @@ public function processNode( return [ $addTip(RuleErrorBuilder::message(sprintf( 'Elseif condition is always %s.', - $exprType->getValue() ? 'true' : 'false' + $exprType->getValue() ? 'true' : 'false', )))->line($node->cond->getLine()) ->identifier('deadCode.elseifConstantCondition') ->metadata([ diff --git a/src/Rules/Comparison/IfConstantConditionRule.php b/src/Rules/Comparison/IfConstantConditionRule.php index f597146def..0de33d796b 100644 --- a/src/Rules/Comparison/IfConstantConditionRule.php +++ b/src/Rules/Comparison/IfConstantConditionRule.php @@ -2,36 +2,34 @@ namespace PHPStan\Rules\Comparison; +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\If_> + * @implements Rule */ -class IfConstantConditionRule implements \PHPStan\Rules\Rule +class IfConstantConditionRule implements Rule { - private ConstantConditionRuleHelper $helper; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ConstantConditionRuleHelper $helper, - bool $treatPhpDocTypesAsCertain + private ConstantConditionRuleHelper $helper, + private bool $treatPhpDocTypesAsCertain, ) { - $this->helper = $helper; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function getNodeType(): string { - return \PhpParser\Node\Stmt\If_::class; + return Node\Stmt\If_::class; } public function processNode( - \PhpParser\Node $node, - \PHPStan\Analyser\Scope $scope + Node $node, + Scope $scope, ): array { $exprType = $this->helper->getBooleanType($scope, $node->cond); @@ -52,7 +50,7 @@ public function processNode( return [ $addTip(RuleErrorBuilder::message(sprintf( 'If condition is always %s.', - $exprType->getValue() ? 'true' : 'false' + $exprType->getValue() ? 'true' : 'false', )))->line($node->cond->getLine()) ->identifier('deadCode.ifConstantCondition') ->metadata([ diff --git a/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php index f873a37332..e60656e751 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php @@ -4,34 +4,28 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function sprintf; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\FuncCall> + * @implements Rule */ -class ImpossibleCheckTypeFunctionCallRule implements \PHPStan\Rules\Rule +class ImpossibleCheckTypeFunctionCallRule implements Rule { - private \PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper $impossibleCheckTypeHelper; - - private bool $checkAlwaysTrueCheckTypeFunctionCall; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, - bool $checkAlwaysTrueCheckTypeFunctionCall, - bool $treatPhpDocTypesAsCertain + private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, + private bool $checkAlwaysTrueCheckTypeFunctionCall, + private bool $treatPhpDocTypesAsCertain, ) { - $this->impossibleCheckTypeHelper = $impossibleCheckTypeHelper; - $this->checkAlwaysTrueCheckTypeFunctionCall = $checkAlwaysTrueCheckTypeFunctionCall; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function getNodeType(): string { - return \PhpParser\Node\Expr\FuncCall::class; + return Node\Expr\FuncCall::class; } public function processNode(Node $node, Scope $scope): array @@ -67,7 +61,7 @@ public function processNode(Node $node, Scope $scope): array $addTip(RuleErrorBuilder::message(sprintf( 'Call to function %s()%s will always evaluate to false.', $functionName, - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->args) + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), )))->build(), ]; } elseif ($this->checkAlwaysTrueCheckTypeFunctionCall) { @@ -75,7 +69,7 @@ public function processNode(Node $node, Scope $scope): array $addTip(RuleErrorBuilder::message(sprintf( 'Call to function %s()%s will always evaluate to true.', $functionName, - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->args) + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), )))->build(), ]; } diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index a49038600a..5c7c8fc0a8 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Comparison; +use PhpParser\Node; use PhpParser\Node\Arg; use PhpParser\Node\Expr; use PhpParser\Node\Expr\FuncCall; @@ -16,55 +17,50 @@ use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; +use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VerbosityLevel; +use function array_column; +use function array_map; +use function array_pop; +use function count; +use function implode; +use function in_array; +use function is_string; +use function reset; +use function sprintf; +use function strtolower; class ImpossibleCheckTypeHelper { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; - - /** @var string[] */ - private array $universalObjectCratesClasses; - - private bool $treatPhpDocTypesAsCertain; - /** - * @param \PHPStan\Reflection\ReflectionProvider $reflectionProvider - * @param \PHPStan\Analyser\TypeSpecifier $typeSpecifier * @param string[] $universalObjectCratesClasses - * @param bool $treatPhpDocTypesAsCertain */ public function __construct( - ReflectionProvider $reflectionProvider, - TypeSpecifier $typeSpecifier, - array $universalObjectCratesClasses, - bool $treatPhpDocTypesAsCertain + private ReflectionProvider $reflectionProvider, + private TypeSpecifier $typeSpecifier, + private array $universalObjectCratesClasses, + private bool $treatPhpDocTypesAsCertain, ) { - $this->reflectionProvider = $reflectionProvider; - $this->typeSpecifier = $typeSpecifier; - $this->universalObjectCratesClasses = $universalObjectCratesClasses; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function findSpecifiedType( Scope $scope, - Expr $node + Expr $node, ): ?bool { if ( $node instanceof FuncCall - && count($node->args) > 0 + && count($node->getArgs()) > 0 ) { - if ($node->name instanceof \PhpParser\Node\Name) { + if ($node->name instanceof Node\Name) { $functionName = strtolower((string) $node->name); if ($functionName === 'assert') { - $assertValue = $scope->getType($node->args[0]->value)->toBoolean(); + $assertValue = $scope->getType($node->getArgs()[0]->value)->toBoolean(); if (!$assertValue instanceof ConstantBooleanType) { return null; } @@ -78,15 +74,15 @@ public function findSpecifiedType( ], true)) { return null; } - if ($functionName === 'count') { + if (in_array($functionName, ['count', 'sizeof'], true)) { return null; } elseif ($functionName === 'defined') { return null; } elseif ( $functionName === 'in_array' - && count($node->args) >= 3 + && count($node->getArgs()) >= 3 ) { - $haystackType = $scope->getType($node->args[1]->value); + $haystackType = $scope->getType($node->getArgs()[1]->value); if ($haystackType instanceof MixedType) { return null; } @@ -95,17 +91,32 @@ public function findSpecifiedType( return null; } - if (!$haystackType instanceof ConstantArrayType || count($haystackType->getValueTypes()) > 0) { - $needleType = $scope->getType($node->args[0]->value); + $constantArrays = TypeUtils::getConstantArrays($haystackType); + $needleType = $scope->getType($node->getArgs()[0]->value); + $valueType = $haystackType->getIterableValueType(); + $constantNeedleTypesCount = count(TypeUtils::getConstantScalars($needleType)); + $constantHaystackTypesCount = count(TypeUtils::getConstantScalars($valueType)); + $isNeedleSupertype = $needleType->isSuperTypeOf($valueType); + if (count($constantArrays) === 0) { + if ($haystackType->isIterableAtLeastOnce()->yes()) { + if ($constantNeedleTypesCount === 1 && $constantHaystackTypesCount === 1) { + if ($isNeedleSupertype->yes()) { + return true; + } + if ($isNeedleSupertype->no()) { + return false; + } + } + } + return null; + } + if (!$haystackType instanceof ConstantArrayType || count($haystackType->getValueTypes()) > 0) { $haystackArrayTypes = TypeUtils::getArrays($haystackType); if (count($haystackArrayTypes) === 1 && $haystackArrayTypes[0]->getIterableValueType() instanceof NeverType) { return null; } - $valueType = $haystackType->getIterableValueType(); - $isNeedleSupertype = $needleType->isSuperTypeOf($valueType); - if ($isNeedleSupertype->maybe() || $isNeedleSupertype->yes()) { foreach ($haystackArrayTypes as $haystackArrayType) { foreach (TypeUtils::getConstantScalars($haystackArrayType->getIterableValueType()) as $constantScalarType) { @@ -119,22 +130,19 @@ public function findSpecifiedType( } if ($isNeedleSupertype->yes()) { - $hasConstantNeedleTypes = count(TypeUtils::getConstantScalars($needleType)) > 0; - $hasConstantHaystackTypes = count(TypeUtils::getConstantScalars($valueType)) > 0; + $hasConstantNeedleTypes = $constantNeedleTypesCount > 0; + $hasConstantHaystackTypes = $constantHaystackTypesCount > 0; if ( - ( - !$hasConstantNeedleTypes - && !$hasConstantHaystackTypes - ) + (!$hasConstantNeedleTypes && !$hasConstantHaystackTypes) || $hasConstantNeedleTypes !== $hasConstantHaystackTypes ) { return null; } } } - } elseif ($functionName === 'method_exists' && count($node->args) >= 2) { - $objectType = $scope->getType($node->args[0]->value); - $methodType = $scope->getType($node->args[1]->value); + } elseif ($functionName === 'method_exists' && count($node->getArgs()) >= 2) { + $objectType = $scope->getType($node->getArgs()[0]->value); + $methodType = $scope->getType($node->getArgs()[1]->value); if ($objectType instanceof ConstantStringType && !$this->reflectionProvider->hasClass($objectType->getValue()) @@ -162,6 +170,12 @@ public function findSpecifiedType( } $specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $node, TypeSpecifierContext::createTruthy()); + + // don't validate types on overwrite + if ($specifiedTypes->shouldOverwrite()) { + return null; + } + $sureTypes = $specifiedTypes->getSureTypes(); $sureNotTypes = $specifiedTypes->getSureNotTypes(); @@ -193,7 +207,7 @@ public function findSpecifiedType( $argumentType = $scope->getNativeType($sureType[0]); } - /** @var \PHPStan\Type\Type $resultType */ + /** @var Type $resultType */ $resultType = $sureType[1]; $isSuperType = $resultType->isSuperTypeOf($argumentType); @@ -216,7 +230,7 @@ public function findSpecifiedType( $argumentType = $scope->getNativeType($sureNotType[0]); } - /** @var \PHPStan\Type\Type $resultType */ + /** @var Type $resultType */ $resultType = $sureNotType[1]; $isSuperType = $resultType->isSuperTypeOf($argumentType); @@ -236,7 +250,7 @@ public function findSpecifiedType( } } $types = TypeCombinator::union( - ...array_column($sureTypes, 1) + ...array_column($sureTypes, 1), ); if ($types instanceof NeverType) { return false; @@ -250,7 +264,7 @@ public function findSpecifiedType( } } $types = TypeCombinator::union( - ...array_column($sureNotTypes, 1) + ...array_column($sureNotTypes, 1), ); if ($types instanceof NeverType) { return true; @@ -261,22 +275,18 @@ public function findSpecifiedType( } /** - * @param Scope $scope - * @param \PhpParser\Node\Arg[] $args - * @return string + * @param Node\Arg[] $args */ public function getArgumentsDescription( Scope $scope, - array $args + array $args, ): string { if (count($args) === 0) { return ''; } - $descriptions = array_map(static function (Arg $arg) use ($scope): string { - return $scope->getType($arg->value)->describe(VerbosityLevel::value()); - }, $args); + $descriptions = array_map(static fn (Arg $arg): string => $scope->getType($arg->value)->describe(VerbosityLevel::value()), $args); if (count($descriptions) < 3) { return sprintf(' with %s', implode(' and ', $descriptions)); @@ -287,7 +297,7 @@ public function getArgumentsDescription( return sprintf( ' with arguments %s and %s', implode(', ', $descriptions), - $lastDescription + $lastDescription, ); } @@ -301,7 +311,7 @@ public function doNotTreatPhpDocTypesAsCertain(): self $this->reflectionProvider, $this->typeSpecifier, $this->universalObjectCratesClasses, - false + false, ); } diff --git a/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php index c7df381e21..6208b0eddf 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php @@ -6,34 +6,28 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\MethodCall> + * @implements Rule */ -class ImpossibleCheckTypeMethodCallRule implements \PHPStan\Rules\Rule +class ImpossibleCheckTypeMethodCallRule implements Rule { - private \PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper $impossibleCheckTypeHelper; - - private bool $checkAlwaysTrueCheckTypeFunctionCall; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, - bool $checkAlwaysTrueCheckTypeFunctionCall, - bool $treatPhpDocTypesAsCertain + private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, + private bool $checkAlwaysTrueCheckTypeFunctionCall, + private bool $treatPhpDocTypesAsCertain, ) { - $this->impossibleCheckTypeHelper = $impossibleCheckTypeHelper; - $this->checkAlwaysTrueCheckTypeFunctionCall = $checkAlwaysTrueCheckTypeFunctionCall; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function getNodeType(): string { - return \PhpParser\Node\Expr\MethodCall::class; + return Node\Expr\MethodCall::class; } public function processNode(Node $node, Scope $scope): array @@ -67,7 +61,7 @@ public function processNode(Node $node, Scope $scope): array 'Call to method %s::%s()%s will always evaluate to false.', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->args) + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), )))->build(), ]; } elseif ($this->checkAlwaysTrueCheckTypeFunctionCall) { @@ -77,7 +71,7 @@ public function processNode(Node $node, Scope $scope): array 'Call to method %s::%s()%s will always evaluate to true.', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->args) + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), )))->build(), ]; } @@ -88,13 +82,13 @@ public function processNode(Node $node, Scope $scope): array private function getMethod( Expr $var, string $methodName, - Scope $scope + Scope $scope, ): MethodReflection { $calledOnType = $scope->getType($var); $method = $scope->getMethodReflection($calledOnType, $methodName); if ($method === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $method; diff --git a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php index 7284114a6a..af7bd57afa 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php @@ -6,34 +6,28 @@ use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\StaticCall> + * @implements Rule */ -class ImpossibleCheckTypeStaticMethodCallRule implements \PHPStan\Rules\Rule +class ImpossibleCheckTypeStaticMethodCallRule implements Rule { - private \PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper $impossibleCheckTypeHelper; - - private bool $checkAlwaysTrueCheckTypeFunctionCall; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, - bool $checkAlwaysTrueCheckTypeFunctionCall, - bool $treatPhpDocTypesAsCertain + private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, + private bool $checkAlwaysTrueCheckTypeFunctionCall, + private bool $treatPhpDocTypesAsCertain, ) { - $this->impossibleCheckTypeHelper = $impossibleCheckTypeHelper; - $this->checkAlwaysTrueCheckTypeFunctionCall = $checkAlwaysTrueCheckTypeFunctionCall; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function getNodeType(): string { - return \PhpParser\Node\Expr\StaticCall::class; + return Node\Expr\StaticCall::class; } public function processNode(Node $node, Scope $scope): array @@ -68,7 +62,7 @@ public function processNode(Node $node, Scope $scope): array 'Call to static method %s::%s()%s will always evaluate to false.', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->args) + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), )))->build(), ]; } elseif ($this->checkAlwaysTrueCheckTypeFunctionCall) { @@ -79,7 +73,7 @@ public function processNode(Node $node, Scope $scope): array 'Call to static method %s::%s()%s will always evaluate to true.', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->args) + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), )))->build(), ]; } @@ -89,15 +83,12 @@ public function processNode(Node $node, Scope $scope): array /** * @param Node\Name|Expr $class - * @param string $methodName - * @param Scope $scope - * @return MethodReflection - * @throws \PHPStan\ShouldNotHappenException + * @throws ShouldNotHappenException */ private function getMethod( $class, string $methodName, - Scope $scope + Scope $scope, ): MethodReflection { if ($class instanceof Node\Name) { @@ -108,7 +99,7 @@ private function getMethod( $method = $scope->getMethodReflection($calledOnType, $methodName); if ($method === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $method; diff --git a/src/Rules/Comparison/MatchExpressionRule.php b/src/Rules/Comparison/MatchExpressionRule.php index 51db3018db..6e89dde2b4 100644 --- a/src/Rules/Comparison/MatchExpressionRule.php +++ b/src/Rules/Comparison/MatchExpressionRule.php @@ -11,6 +11,8 @@ use PHPStan\Type\NeverType; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function count; +use function sprintf; /** * @implements Rule @@ -18,11 +20,8 @@ class MatchExpressionRule implements Rule { - private bool $checkAlwaysTrueStrictComparison; - - public function __construct(bool $checkAlwaysTrueStrictComparison) + public function __construct(private bool $checkAlwaysTrueStrictComparison) { - $this->checkAlwaysTrueStrictComparison = $checkAlwaysTrueStrictComparison; } public function getNodeType(): string @@ -50,7 +49,7 @@ public function processNode(Node $node, Scope $scope): array $armConditionScope = $armCondition->getScope(); $armConditionExpr = new Node\Expr\BinaryOp\Identical( $matchCondition, - $armCondition->getCondition() + $armCondition->getCondition(), ); $armConditionResult = $armConditionScope->getType($armConditionExpr); if (!$armConditionResult instanceof ConstantBooleanType) { @@ -62,7 +61,7 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( 'Match arm comparison between %s and %s is always false.', $armConditionScope->getType($matchCondition)->describe(VerbosityLevel::value()), - $armConditionScope->getType($armCondition->getCondition())->describe(VerbosityLevel::value()) + $armConditionScope->getType($armCondition->getCondition())->describe(VerbosityLevel::value()), ))->line($armLine)->build(); } else { $nextArmIsDead = true; @@ -73,7 +72,7 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( 'Match arm comparison between %s and %s is always true.', $armConditionScope->getType($matchCondition)->describe(VerbosityLevel::value()), - $armConditionScope->getType($armCondition->getCondition())->describe(VerbosityLevel::value()) + $armConditionScope->getType($armCondition->getCondition())->describe(VerbosityLevel::value()), ))->line($armLine)->build(); } } @@ -86,7 +85,7 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( 'Match expression does not handle remaining %s: %s', $remainingType instanceof UnionType ? 'values' : 'value', - $remainingType->describe(VerbosityLevel::value()) + $remainingType->describe(VerbosityLevel::value()), ))->build(); } } diff --git a/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php b/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php index de0ebaf218..bf8feaaec6 100644 --- a/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php +++ b/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php @@ -2,15 +2,19 @@ namespace PHPStan\Rules\Comparison; +use PhpParser\Node; use PhpParser\Node\Expr\BinaryOp; +use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\BinaryOp> + * @implements Rule */ -class NumberComparisonOperatorsConstantConditionRule implements \PHPStan\Rules\Rule +class NumberComparisonOperatorsConstantConditionRule implements Rule { public function getNodeType(): string @@ -19,8 +23,8 @@ public function getNodeType(): string } public function processNode( - \PhpParser\Node $node, - \PHPStan\Analyser\Scope $scope + Node $node, + Scope $scope, ): array { if ( @@ -40,7 +44,7 @@ public function processNode( $node->getOperatorSigil(), $scope->getType($node->left)->describe(VerbosityLevel::value()), $scope->getType($node->right)->describe(VerbosityLevel::value()), - $exprType->getValue() ? 'true' : 'false' + $exprType->getValue() ? 'true' : 'false', ))->build(), ]; } diff --git a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php index e09838544a..18200745a0 100644 --- a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php +++ b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php @@ -4,21 +4,20 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\BinaryOp> + * @implements Rule */ -class StrictComparisonOfDifferentTypesRule implements \PHPStan\Rules\Rule +class StrictComparisonOfDifferentTypesRule implements Rule { - private bool $checkAlwaysTrueStrictComparison; - - public function __construct(bool $checkAlwaysTrueStrictComparison) + public function __construct(private bool $checkAlwaysTrueStrictComparison) { - $this->checkAlwaysTrueStrictComparison = $checkAlwaysTrueStrictComparison; } public function getNodeType(): string @@ -46,7 +45,7 @@ public function processNode(Node $node, Scope $scope): array '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()) + $rightType->describe(VerbosityLevel::value()), ))->build(), ]; } elseif ($this->checkAlwaysTrueStrictComparison) { @@ -55,7 +54,7 @@ public function processNode(Node $node, Scope $scope): array '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()) + $rightType->describe(VerbosityLevel::value()), ))->build(), ]; } diff --git a/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php b/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php index 5e1e96b3a9..e608d7bfbd 100644 --- a/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php +++ b/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php @@ -2,36 +2,34 @@ namespace PHPStan\Rules\Comparison; +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Ternary> + * @implements Rule */ -class TernaryOperatorConstantConditionRule implements \PHPStan\Rules\Rule +class TernaryOperatorConstantConditionRule implements Rule { - private ConstantConditionRuleHelper $helper; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ConstantConditionRuleHelper $helper, - bool $treatPhpDocTypesAsCertain + private ConstantConditionRuleHelper $helper, + private bool $treatPhpDocTypesAsCertain, ) { - $this->helper = $helper; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function getNodeType(): string { - return \PhpParser\Node\Expr\Ternary::class; + return Node\Expr\Ternary::class; } public function processNode( - \PhpParser\Node $node, - \PHPStan\Analyser\Scope $scope + Node $node, + Scope $scope, ): array { $exprType = $this->helper->getBooleanType($scope, $node->cond); @@ -51,7 +49,7 @@ public function processNode( return [ $addTip(RuleErrorBuilder::message(sprintf( 'Ternary operator condition is always %s.', - $exprType->getValue() ? 'true' : 'false' + $exprType->getValue() ? 'true' : 'false', ))) ->identifier('deadCode.ternaryConstantCondition') ->metadata([ diff --git a/src/Rules/Comparison/UnreachableIfBranchesRule.php b/src/Rules/Comparison/UnreachableIfBranchesRule.php index e26b63d748..ce9eb2328f 100644 --- a/src/Rules/Comparison/UnreachableIfBranchesRule.php +++ b/src/Rules/Comparison/UnreachableIfBranchesRule.php @@ -4,26 +4,21 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantBooleanType; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\If_> + * @implements Rule */ -class UnreachableIfBranchesRule implements \PHPStan\Rules\Rule +class UnreachableIfBranchesRule implements Rule { - private ConstantConditionRuleHelper $helper; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ConstantConditionRuleHelper $helper, - bool $treatPhpDocTypesAsCertain + private ConstantConditionRuleHelper $helper, + private bool $treatPhpDocTypesAsCertain, ) { - $this->helper = $helper; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function getNodeType(): string diff --git a/src/Rules/Comparison/UnreachableTernaryElseBranchRule.php b/src/Rules/Comparison/UnreachableTernaryElseBranchRule.php index 2c9a217bf0..551f0e63e9 100644 --- a/src/Rules/Comparison/UnreachableTernaryElseBranchRule.php +++ b/src/Rules/Comparison/UnreachableTernaryElseBranchRule.php @@ -9,22 +9,16 @@ use PHPStan\Type\Constant\ConstantBooleanType; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Ternary> + * @implements Rule */ class UnreachableTernaryElseBranchRule implements Rule { - private ConstantConditionRuleHelper $helper; - - private bool $treatPhpDocTypesAsCertain; - public function __construct( - ConstantConditionRuleHelper $helper, - bool $treatPhpDocTypesAsCertain + private ConstantConditionRuleHelper $helper, + private bool $treatPhpDocTypesAsCertain, ) { - $this->helper = $helper; - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; } public function getNodeType(): string diff --git a/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php b/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php new file mode 100644 index 0000000000..0c485c7329 --- /dev/null +++ b/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php @@ -0,0 +1,59 @@ + + */ +class WhileLoopAlwaysFalseConditionRule implements Rule +{ + + public function __construct( + private ConstantConditionRuleHelper $helper, + private bool $treatPhpDocTypesAsCertain, + ) + { + } + + public function getNodeType(): string + { + return While_::class; + } + + public function processNode( + Node $node, + Scope $scope, + ): array + { + $exprType = $this->helper->getBooleanType($scope, $node->cond); + if ($exprType instanceof ConstantBooleanType && !$exprType->getValue()) { + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { + if (!$this->treatPhpDocTypesAsCertain) { + return $ruleErrorBuilder; + } + + $booleanNativeType = $this->helper->getNativeBooleanType($scope, $node->cond); + if ($booleanNativeType instanceof ConstantBooleanType) { + return $ruleErrorBuilder; + } + + return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + }; + + return [ + $addTip(RuleErrorBuilder::message('While loop condition is always false.'))->line($node->cond->getLine()) + ->build(), + ]; + } + + return []; + } + +} diff --git a/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php b/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php new file mode 100644 index 0000000000..3723019229 --- /dev/null +++ b/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php @@ -0,0 +1,86 @@ + + */ +class WhileLoopAlwaysTrueConditionRule implements Rule +{ + + public function __construct( + private ConstantConditionRuleHelper $helper, + private bool $treatPhpDocTypesAsCertain, + ) + { + } + + public function getNodeType(): string + { + return BreaklessWhileLoopNode::class; + } + + public function processNode( + Node $node, + Scope $scope, + ): array + { + foreach ($node->getExitPoints() as $exitPoint) { + $statement = $exitPoint->getStatement(); + if ($statement instanceof Break_) { + return []; + } + if (!$statement instanceof Continue_) { + return []; + } + if ($statement->num === null) { + continue; + } + if (!$statement->num instanceof LNumber) { + continue; + } + $value = $statement->num->value; + if ($value === 1) { + continue; + } + + if ($value > 1) { + return []; + } + } + $originalNode = $node->getOriginalNode(); + $exprType = $this->helper->getBooleanType($scope, $originalNode->cond); + if ($exprType instanceof ConstantBooleanType && $exprType->getValue()) { + $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { + if (!$this->treatPhpDocTypesAsCertain) { + return $ruleErrorBuilder; + } + + $booleanNativeType = $this->helper->getNativeBooleanType($scope, $originalNode->cond); + if ($booleanNativeType instanceof ConstantBooleanType) { + return $ruleErrorBuilder; + } + + return $ruleErrorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'); + }; + + return [ + $addTip(RuleErrorBuilder::message('While loop condition is always true.'))->line($originalNode->cond->getLine()) + ->build(), + ]; + } + + return []; + } + +} diff --git a/src/Rules/Constants/ConstantRule.php b/src/Rules/Constants/ConstantRule.php index 8770d20c2f..1eb08445dd 100644 --- a/src/Rules/Constants/ConstantRule.php +++ b/src/Rules/Constants/ConstantRule.php @@ -4,12 +4,14 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\ConstFetch> + * @implements Rule */ -class ConstantRule implements \PHPStan\Rules\Rule +class ConstantRule implements Rule { public function getNodeType(): string @@ -23,7 +25,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message(sprintf( 'Constant %s not found.', - (string) $node->name + (string) $node->name, ))->discoveringSymbolsTip()->build(), ]; } diff --git a/src/Rules/Constants/FinalConstantRule.php b/src/Rules/Constants/FinalConstantRule.php new file mode 100644 index 0000000000..23fa67ab65 --- /dev/null +++ b/src/Rules/Constants/FinalConstantRule.php @@ -0,0 +1,40 @@ + */ +class FinalConstantRule implements Rule +{ + + public function __construct(private PhpVersion $phpVersion) + { + } + + public function getNodeType(): string + { + return ClassConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->isFinal()) { + return []; + } + + if ($this->phpVersion->supportsFinalConstants()) { + return []; + } + + return [ + RuleErrorBuilder::message('Final class constants are supported only on PHP 8.1 and later.')->nonIgnorable()->build(), + ]; + } + +} diff --git a/src/Rules/Constants/LazyAlwaysUsedClassConstantsExtensionProvider.php b/src/Rules/Constants/LazyAlwaysUsedClassConstantsExtensionProvider.php index 80a5424321..895fb96360 100644 --- a/src/Rules/Constants/LazyAlwaysUsedClassConstantsExtensionProvider.php +++ b/src/Rules/Constants/LazyAlwaysUsedClassConstantsExtensionProvider.php @@ -7,14 +7,11 @@ class LazyAlwaysUsedClassConstantsExtensionProvider implements AlwaysUsedClassConstantsExtensionProvider { - private Container $container; - /** @var AlwaysUsedClassConstantsExtension[]|null */ private ?array $extensions = null; - public function __construct(Container $container) + public function __construct(private Container $container) { - $this->container = $container; } public function getExtensions(): array diff --git a/src/Rules/Constants/MissingClassConstantTypehintRule.php b/src/Rules/Constants/MissingClassConstantTypehintRule.php new file mode 100644 index 0000000000..25f536e43b --- /dev/null +++ b/src/Rules/Constants/MissingClassConstantTypehintRule.php @@ -0,0 +1,89 @@ + + */ +final class MissingClassConstantTypehintRule implements Rule +{ + + public function __construct(private MissingTypehintCheck $missingTypehintCheck) + { + } + + public function getNodeType(): string + { + return Node\Stmt\ClassConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } + + $errors = []; + foreach ($node->consts as $const) { + $constantName = $const->name->toString(); + $errors = array_merge($errors, $this->processSingleConstant($scope->getClassReflection(), $constantName)); + } + + return $errors; + } + + /** + * @return RuleError[] + */ + private function processSingleConstant(ClassReflection $classReflection, string $constantName): array + { + $constantReflection = $classReflection->getConstant($constantName); + $constantType = $constantReflection->getValueType(); + + $errors = []; + foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($constantType) as $iterableType) { + $iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly()); + $errors[] = RuleErrorBuilder::message(sprintf( + 'Constant %s::%s type has no value type specified in iterable type %s.', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantName, + $iterableTypeDescription, + ))->tip(MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP)->build(); + } + + foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($constantType) as [$name, $genericTypeNames]) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Constant %s::%s with generic %s does not specify its types: %s', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantName, + $name, + implode(', ', $genericTypeNames), + ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); + } + + foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($constantType) as $callableType) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Constant %s::%s type has no signature specified for %s.', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantName, + $callableType->describe(VerbosityLevel::typeOnly()), + ))->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Constants/OverridingConstantRule.php b/src/Rules/Constants/OverridingConstantRule.php new file mode 100644 index 0000000000..ca60d2c9b8 --- /dev/null +++ b/src/Rules/Constants/OverridingConstantRule.php @@ -0,0 +1,149 @@ + + */ +class OverridingConstantRule implements Rule +{ + + public function __construct( + private bool $checkPhpDocMethodSignatures, + ) + { + } + + public function getNodeType(): string + { + return Node\Stmt\ClassConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } + + $errors = []; + foreach ($node->consts as $const) { + $constantName = $const->name->toString(); + $errors = array_merge($errors, $this->processSingleConstant($scope->getClassReflection(), $constantName)); + } + + return $errors; + } + + /** + * @return RuleError[] + */ + private function processSingleConstant(ClassReflection $classReflection, string $constantName): array + { + $prototype = $this->findPrototype($classReflection, $constantName); + if (!$prototype instanceof ClassConstantReflection) { + return []; + } + + $constantReflection = $classReflection->getConstant($constantName); + if (!$constantReflection instanceof ClassConstantReflection) { + return []; + } + + $errors = []; + if ($prototype->isFinal()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Constant %s::%s overrides final constant %s::%s.', + $classReflection->getDisplayName(), + $constantReflection->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $prototype->getName(), + ))->nonIgnorable()->build(); + } + + if ($prototype->isPublic()) { + if (!$constantReflection->isPublic()) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s constant %s::%s overriding public constant %s::%s should also be public.', + $constantReflection->isPrivate() ? 'Private' : 'Protected', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantReflection->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $prototype->getName(), + ))->nonIgnorable()->build(); + } + } elseif ($constantReflection->isPrivate()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Private constant %s::%s overriding protected constant %s::%s should be protected or public.', + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantReflection->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $prototype->getName(), + ))->nonIgnorable()->build(); + } + + if (!$this->checkPhpDocMethodSignatures) { + return $errors; + } + + if (!$prototype->hasPhpDocType()) { + return $errors; + } + + if (!$constantReflection->hasPhpDocType()) { + return $errors; + } + + if (!$prototype->getValueType()->isSuperTypeOf($constantReflection->getValueType())->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Type %s of constant %s::%s is not covariant with type %s of constant %s::%s.', + $constantReflection->getValueType()->describe(VerbosityLevel::value()), + $constantReflection->getDeclaringClass()->getDisplayName(), + $constantReflection->getName(), + $prototype->getValueType()->describe(VerbosityLevel::value()), + $prototype->getDeclaringClass()->getDisplayName(), + $prototype->getName(), + ))->build(); + } + + return $errors; + } + + private function findPrototype(ClassReflection $classReflection, string $constantName): ?ConstantReflection + { + foreach ($classReflection->getImmediateInterfaces() as $immediateInterface) { + if ($immediateInterface->hasConstant($constantName)) { + return $immediateInterface->getConstant($constantName); + } + } + + $parentClass = $classReflection->getParentClass(); + if ($parentClass === null) { + return null; + } + + if (!$parentClass->hasConstant($constantName)) { + return null; + } + + $constant = $parentClass->getConstant($constantName); + if ($constant->isPrivate()) { + return null; + } + + return $constant; + } + +} diff --git a/src/Rules/DateTimeInstantiationRule.php b/src/Rules/DateTimeInstantiationRule.php index 0e8ead4972..3b09ccef61 100644 --- a/src/Rules/DateTimeInstantiationRule.php +++ b/src/Rules/DateTimeInstantiationRule.php @@ -7,11 +7,16 @@ use PhpParser\Node\Expr\New_; use PHPStan\Analyser\Scope; use PHPStan\Type\Constant\ConstantStringType; +use Throwable; +use function count; +use function in_array; +use function sprintf; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\New_> + * @implements Rule */ -class DateTimeInstantiationRule implements \PHPStan\Rules\Rule +class DateTimeInstantiationRule implements Rule { public function getNodeType(): string @@ -25,14 +30,14 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { if ( - !($node->class instanceof \PhpParser\Node\Name) - || \count($node->args) === 0 - || !\in_array(strtolower((string) $node->class), ['datetime', 'datetimeimmutable'], true) + !($node->class instanceof Node\Name) + || count($node->getArgs()) === 0 + || !in_array(strtolower((string) $node->class), ['datetime', 'datetimeimmutable'], true) ) { return []; } - $arg = $scope->getType($node->args[0]->value); + $arg = $scope->getType($node->getArgs()[0]->value); if (!($arg instanceof ConstantStringType)) { return []; } @@ -41,7 +46,7 @@ public function processNode(Node $node, Scope $scope): array $dateString = $arg->getValue(); try { new DateTime($dateString); - } catch (\Throwable $e) { + } catch (Throwable) { // an exception is thrown for errors only but we want to catch warnings too } $lastErrors = DateTime::getLastErrors(); @@ -51,7 +56,7 @@ public function processNode(Node $node, Scope $scope): array 'Instantiating %s with %s produces an error: %s', (string) $node->class, $dateString, - $error + $error, ))->build(); } } diff --git a/src/Rules/DeadCode/NoopRule.php b/src/Rules/DeadCode/NoopRule.php index fa7a42c9dd..a910a3276b 100644 --- a/src/Rules/DeadCode/NoopRule.php +++ b/src/Rules/DeadCode/NoopRule.php @@ -7,18 +7,16 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Expression> + * @implements Rule */ class NoopRule implements Rule { - private Standard $printer; - - public function __construct(Standard $printer) + public function __construct(private Standard $printer) { - $this->printer = $printer; } public function getNodeType(): string @@ -56,7 +54,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message(sprintf( 'Expression "%s" on a separate line does not do anything.', - $this->printer->prettyPrintExpr($originalExpr) + $this->printer->prettyPrintExpr($originalExpr), ))->line($expr->getLine()) ->identifier('deadCode.noopExpression') ->metadata([ diff --git a/src/Rules/DeadCode/UnreachableStatementRule.php b/src/Rules/DeadCode/UnreachableStatementRule.php index 67a7105658..8f4793267a 100644 --- a/src/Rules/DeadCode/UnreachableStatementRule.php +++ b/src/Rules/DeadCode/UnreachableStatementRule.php @@ -9,7 +9,7 @@ use PHPStan\Rules\RuleErrorBuilder; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\UnreachableStatementNode> + * @implements Rule */ class UnreachableStatementRule implements Rule { diff --git a/src/Rules/DeadCode/UnusedPrivateConstantRule.php b/src/Rules/DeadCode/UnusedPrivateConstantRule.php index 6b3006f87f..c41e3d66eb 100644 --- a/src/Rules/DeadCode/UnusedPrivateConstantRule.php +++ b/src/Rules/DeadCode/UnusedPrivateConstantRule.php @@ -8,6 +8,9 @@ use PHPStan\Rules\Constants\AlwaysUsedClassConstantsExtensionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; +use PHPStan\Type\TypeWithClassName; +use function sprintf; /** * @implements Rule @@ -15,11 +18,8 @@ class UnusedPrivateConstantRule implements Rule { - private AlwaysUsedClassConstantsExtensionProvider $extensionProvider; - - public function __construct(AlwaysUsedClassConstantsExtensionProvider $extensionProvider) + public function __construct(private AlwaysUsedClassConstantsExtensionProvider $extensionProvider) { - $this->extensionProvider = $extensionProvider; } public function getNodeType(): string @@ -29,11 +29,11 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$node->getClass() instanceof Node\Stmt\Class_) { + if (!$node->getClass() instanceof Node\Stmt\Class_ && !$node->getClass() instanceof Node\Stmt\Enum_) { return []; } if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $classReflection = $scope->getClassReflection(); @@ -60,17 +60,26 @@ public function processNode(Node $node, Scope $scope): array foreach ($node->getFetches() as $fetch) { $fetchNode = $fetch->getNode(); - if (!$fetchNode->class instanceof Node\Name) { - continue; - } if (!$fetchNode->name instanceof Node\Identifier) { continue; } - $fetchScope = $fetch->getScope(); - $fetchedOnClass = $fetchScope->resolveName($fetchNode->class); - if ($fetchedOnClass !== $classReflection->getName()) { - continue; + + if ($fetchNode->class instanceof Node\Name) { + $fetchScope = $fetch->getScope(); + $fetchedOnClass = $fetchScope->resolveName($fetchNode->class); + if ($fetchedOnClass !== $classReflection->getName()) { + continue; + } + } else { + $classExprType = $fetch->getScope()->getType($fetchNode->class); + if (!$classExprType instanceof TypeWithClassName) { + continue; + } + if ($classExprType->getClassName() !== $classReflection->getName()) { + continue; + } } + unset($constants[$fetchNode->name->toString()]); } @@ -85,6 +94,7 @@ public function processNode(Node $node, Scope $scope): array 'classStartLine' => $node->getClass()->getStartLine(), 'constantName' => $constantName, ]) + ->tip(sprintf('See: %s', 'https://phpstan.org/developing-extensions/always-used-class-constants')) ->build(); } diff --git a/src/Rules/DeadCode/UnusedPrivateMethodRule.php b/src/Rules/DeadCode/UnusedPrivateMethodRule.php index 4d3c3cb2fb..b694ebf9de 100644 --- a/src/Rules/DeadCode/UnusedPrivateMethodRule.php +++ b/src/Rules/DeadCode/UnusedPrivateMethodRule.php @@ -9,11 +9,16 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; use PHPStan\Type\TypeUtils; +use function array_map; +use function count; +use function sprintf; +use function strtolower; /** * @implements Rule @@ -28,11 +33,11 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!$node->getClass() instanceof Node\Stmt\Class_) { + if (!$node->getClass() instanceof Node\Stmt\Class_ && !$node->getClass() instanceof Node\Stmt\Enum_) { return []; } if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $classReflection = $scope->getClassReflection(); $constructor = null; @@ -73,9 +78,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $methodNames = array_map(static function (ConstantStringType $type): string { - return $type->getValue(); - }, $strings); + $methodNames = array_map(static fn (ConstantStringType $type): string => $type->getValue(), $strings); } if ($methodCallNode instanceof Node\Expr\MethodCall) { diff --git a/src/Rules/DeadCode/UnusedPrivatePropertyRule.php b/src/Rules/DeadCode/UnusedPrivatePropertyRule.php index 6ba7338b28..add56a0339 100644 --- a/src/Rules/DeadCode/UnusedPrivatePropertyRule.php +++ b/src/Rules/DeadCode/UnusedPrivatePropertyRule.php @@ -9,10 +9,16 @@ use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; use PHPStan\Type\TypeUtils; +use function array_key_exists; +use function array_map; +use function count; +use function sprintf; +use function strpos; /** * @implements Rule @@ -20,32 +26,17 @@ class UnusedPrivatePropertyRule implements Rule { - private ReadWritePropertiesExtensionProvider $extensionProvider; - - /** @var string[] */ - private array $alwaysWrittenTags; - - /** @var string[] */ - private array $alwaysReadTags; - - private bool $checkUninitializedProperties; - /** - * @param ReadWritePropertiesExtensionProvider $extensionProvider * @param string[] $alwaysWrittenTags * @param string[] $alwaysReadTags */ public function __construct( - ReadWritePropertiesExtensionProvider $extensionProvider, - array $alwaysWrittenTags, - array $alwaysReadTags, - bool $checkUninitializedProperties + private ReadWritePropertiesExtensionProvider $extensionProvider, + private array $alwaysWrittenTags, + private array $alwaysReadTags, + private bool $checkUninitializedProperties, ) { - $this->extensionProvider = $extensionProvider; - $this->alwaysWrittenTags = $alwaysWrittenTags; - $this->alwaysReadTags = $alwaysReadTags; - $this->checkUninitializedProperties = $checkUninitializedProperties; } public function getNodeType(): string @@ -59,7 +50,7 @@ public function processNode(Node $node, Scope $scope): array return []; } if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $classReflection = $scope->getClassReflection(); $classType = new ObjectType($classReflection->getName()); @@ -136,9 +127,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $propertyNames = array_map(static function (ConstantStringType $type): string { - return $type->getValue(); - }, $strings); + $propertyNames = array_map(static fn (ConstantStringType $type): string => $type->getValue(), $strings); } if ($fetch instanceof Node\Expr\PropertyFetch) { $fetchedOnType = $usage->getScope()->getType($fetch->var); @@ -185,6 +174,7 @@ public function processNode(Node $node, Scope $scope): array } else { $propertyName = sprintf('Property %s::$%s', $scope->getClassReflection()->getDisplayName(), $name); } + $tip = sprintf('See: %s', 'https://phpstan.org/developing-extensions/always-read-written-properties'); if (!$data['read']) { if (!$data['written']) { $errors[] = RuleErrorBuilder::message(sprintf('%s is unused.', $propertyName)) @@ -196,12 +186,13 @@ public function processNode(Node $node, Scope $scope): array 'classStartLine' => $node->getClass()->getStartLine(), 'propertyName' => $name, ]) + ->tip($tip) ->build(); } else { - $errors[] = RuleErrorBuilder::message(sprintf('%s is never read, only written.', $propertyName))->line($propertyNode->getStartLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('%s is never read, only written.', $propertyName))->line($propertyNode->getStartLine())->tip($tip)->build(); } } elseif (!$data['written'] && (!array_key_exists($name, $uninitializedProperties) || !$this->checkUninitializedProperties)) { - $errors[] = RuleErrorBuilder::message(sprintf('%s is never written, only read.', $propertyName))->line($propertyNode->getStartLine())->build(); + $errors[] = RuleErrorBuilder::message(sprintf('%s is never written, only read.', $propertyName))->line($propertyNode->getStartLine())->tip($tip)->build(); } } diff --git a/src/Rules/Debug/DumpTypeRule.php b/src/Rules/Debug/DumpTypeRule.php index 6c3c4be802..ceeeefa0e5 100644 --- a/src/Rules/Debug/DumpTypeRule.php +++ b/src/Rules/Debug/DumpTypeRule.php @@ -8,6 +8,9 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\VerbosityLevel; +use function count; +use function sprintf; +use function strtolower; /** * @implements Rule @@ -15,11 +18,8 @@ class DumpTypeRule implements Rule { - private ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct(private ReflectionProvider $reflectionProvider) { - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -42,7 +42,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - if (count($node->args) === 0) { + if (count($node->getArgs()) === 0) { return [ RuleErrorBuilder::message(sprintf('Missing argument for %s() function call.', $functionName)) ->nonIgnorable() @@ -54,8 +54,8 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message( sprintf( 'Dumped type: %s', - $scope->getType($node->args[0]->value)->describe(VerbosityLevel::precise()) - ) + $scope->getType($node->getArgs()[0]->value)->describe(VerbosityLevel::precise()), + ), )->nonIgnorable()->build(), ]; } diff --git a/src/Rules/Debug/FileAssertRule.php b/src/Rules/Debug/FileAssertRule.php index dc0831ae39..d8ebdbfa4a 100644 --- a/src/Rules/Debug/FileAssertRule.php +++ b/src/Rules/Debug/FileAssertRule.php @@ -12,6 +12,9 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\VerbosityLevel; +use function count; +use function is_string; +use function sprintf; /** * @implements Rule @@ -19,11 +22,8 @@ class FileAssertRule implements Rule { - private ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct(private ReflectionProvider $reflectionProvider) { - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -43,15 +43,15 @@ public function processNode(Node $node, Scope $scope): array $function = $this->reflectionProvider->getFunction($node->name, $scope); if ($function->getName() === 'PHPStan\\Testing\\assertType') { - return $this->processAssertType($node->args, $scope); + return $this->processAssertType($node->getArgs(), $scope); } if ($function->getName() === 'PHPStan\\Testing\\assertNativeType') { - return $this->processAssertNativeType($node->args, $scope); + return $this->processAssertNativeType($node->getArgs(), $scope); } if ($function->getName() === 'PHPStan\\Testing\\assertVariableCertainty') { - return $this->processAssertVariableCertainty($node->args, $scope); + return $this->processAssertVariableCertainty($node->getArgs(), $scope); } return []; @@ -59,7 +59,6 @@ public function processNode(Node $node, Scope $scope): array /** * @param Node\Arg[] $args - * @param Scope $scope * @return RuleError[] */ private function processAssertType(array $args, Scope $scope): array @@ -87,7 +86,6 @@ private function processAssertType(array $args, Scope $scope): array /** * @param Node\Arg[] $args - * @param Scope $scope * @return RuleError[] */ private function processAssertNativeType(array $args, Scope $scope): array @@ -116,7 +114,6 @@ private function processAssertNativeType(array $args, Scope $scope): array /** * @param Node\Arg[] $args - * @param Scope $scope * @return RuleError[] */ private function processAssertVariableCertainty(array $args, Scope $scope): array diff --git a/src/Rules/EnumCases/EnumCaseAttributesRule.php b/src/Rules/EnumCases/EnumCaseAttributesRule.php new file mode 100644 index 0000000000..8d584b72f6 --- /dev/null +++ b/src/Rules/EnumCases/EnumCaseAttributesRule.php @@ -0,0 +1,36 @@ + + */ +class EnumCaseAttributesRule implements Rule +{ + + public function __construct(private AttributesCheck $attributesCheck) + { + } + + public function getNodeType(): string + { + return Node\Stmt\EnumCase::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return $this->attributesCheck->check( + $scope, + $node->attrGroups, + Attribute::TARGET_CLASS_CONSTANT, + 'class constant', + ); + } + +} diff --git a/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php b/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php index 025e87690d..4cbf0a7d92 100644 --- a/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php +++ b/src/Rules/Exceptions/CatchWithUnthrownExceptionRule.php @@ -7,7 +7,9 @@ use PHPStan\Node\CatchWithUnthrownExceptionNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Type\NeverType; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** * @implements Rule @@ -22,9 +24,17 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { + if ($node->getCaughtType() instanceof NeverType) { + return [ + RuleErrorBuilder::message( + sprintf('Dead catch - %s is already caught above.', $node->getOriginalCaughtType()->describe(VerbosityLevel::typeOnly())), + )->line($node->getLine())->build(), + ]; + } + return [ RuleErrorBuilder::message( - sprintf('Dead catch - %s is never thrown in the try block.', $node->getCaughtType()->describe(VerbosityLevel::typeOnly())) + sprintf('Dead catch - %s is never thrown in the try block.', $node->getCaughtType()->describe(VerbosityLevel::typeOnly())), )->line($node->getLine())->build(), ]; } diff --git a/src/Rules/Exceptions/CaughtExceptionExistenceRule.php b/src/Rules/Exceptions/CaughtExceptionExistenceRule.php index 13074ff545..bbd4b6be44 100644 --- a/src/Rules/Exceptions/CaughtExceptionExistenceRule.php +++ b/src/Rules/Exceptions/CaughtExceptionExistenceRule.php @@ -8,29 +8,24 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use Throwable; +use function array_merge; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Catch_> + * @implements Rule */ -class CaughtExceptionExistenceRule implements \PHPStan\Rules\Rule +class CaughtExceptionExistenceRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private bool $checkClassCaseSensitivity; - public function __construct( - ReflectionProvider $reflectionProvider, - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - bool $checkClassCaseSensitivity + private ReflectionProvider $reflectionProvider, + private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private bool $checkClassCaseSensitivity, ) { - $this->reflectionProvider = $reflectionProvider; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->checkClassCaseSensitivity = $checkClassCaseSensitivity; } public function getNodeType(): string @@ -52,7 +47,7 @@ public function processNode(Node $node, Scope $scope): array } $classReflection = $this->reflectionProvider->getClass($className); - if (!$classReflection->isInterface() && !$classReflection->implementsInterface(\Throwable::class)) { + if (!$classReflection->isInterface() && !$classReflection->implementsInterface(Throwable::class)) { $errors[] = RuleErrorBuilder::message(sprintf('Caught class %s is not an exception.', $classReflection->getDisplayName()))->line($class->getLine())->build(); } @@ -62,7 +57,7 @@ public function processNode(Node $node, Scope $scope): array $errors = array_merge( $errors, - $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($className, $class)]) + $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($className, $class)]), ); } diff --git a/src/Rules/Exceptions/DeadCatchRule.php b/src/Rules/Exceptions/DeadCatchRule.php deleted file mode 100644 index d05a634792..0000000000 --- a/src/Rules/Exceptions/DeadCatchRule.php +++ /dev/null @@ -1,60 +0,0 @@ - - */ -class DeadCatchRule implements Rule -{ - - public function getNodeType(): string - { - return Node\Stmt\TryCatch::class; - } - - public function processNode(Node $node, Scope $scope): array - { - $catchTypes = array_map(static function (Node\Stmt\Catch_ $catch): Type { - return TypeCombinator::union(...array_map(static function (Node\Name $className): ObjectType { - return new ObjectType($className->toString()); - }, $catch->types)); - }, $node->catches); - $catchesCount = count($catchTypes); - $errors = []; - for ($i = 0; $i < $catchesCount - 1; $i++) { - $firstType = $catchTypes[$i]; - for ($j = $i + 1; $j < $catchesCount; $j++) { - $secondType = $catchTypes[$j]; - if (!$firstType->isSuperTypeOf($secondType)->yes()) { - continue; - } - - $errors[] = RuleErrorBuilder::message(sprintf( - 'Dead catch - %s is already caught by %s above.', - $secondType->describe(VerbosityLevel::typeOnly()), - $firstType->describe(VerbosityLevel::typeOnly()) - ))->line($node->catches[$j]->getLine()) - ->identifier('deadCode.unreachableCatch') - ->metadata([ - 'tryLine' => $node->getLine(), - 'firstCatchOrder' => $i, - 'deadCatchOrder' => $j, - ]) - ->build(); - } - } - - return $errors; - } - -} diff --git a/src/Rules/Exceptions/DefaultExceptionTypeResolver.php b/src/Rules/Exceptions/DefaultExceptionTypeResolver.php index c2f4a03bf1..8da4705142 100644 --- a/src/Rules/Exceptions/DefaultExceptionTypeResolver.php +++ b/src/Rules/Exceptions/DefaultExceptionTypeResolver.php @@ -5,44 +5,25 @@ use Nette\Utils\Strings; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; +use function count; class DefaultExceptionTypeResolver implements ExceptionTypeResolver { - private ReflectionProvider $reflectionProvider; - - /** @var string[] */ - private array $uncheckedExceptionRegexes; - - /** @var string[] */ - private array $uncheckedExceptionClasses; - - /** @var string[] */ - private array $checkedExceptionRegexes; - - /** @var string[] */ - private array $checkedExceptionClasses; - /** - * @param ReflectionProvider $reflectionProvider * @param string[] $uncheckedExceptionRegexes * @param string[] $uncheckedExceptionClasses * @param string[] $checkedExceptionRegexes * @param string[] $checkedExceptionClasses */ public function __construct( - ReflectionProvider $reflectionProvider, - array $uncheckedExceptionRegexes, - array $uncheckedExceptionClasses, - array $checkedExceptionRegexes, - array $checkedExceptionClasses + private ReflectionProvider $reflectionProvider, + private array $uncheckedExceptionRegexes, + private array $uncheckedExceptionClasses, + private array $checkedExceptionRegexes, + private array $checkedExceptionClasses, ) { - $this->reflectionProvider = $reflectionProvider; - $this->uncheckedExceptionRegexes = $uncheckedExceptionRegexes; - $this->uncheckedExceptionClasses = $uncheckedExceptionClasses; - $this->checkedExceptionRegexes = $checkedExceptionRegexes; - $this->checkedExceptionClasses = $checkedExceptionClasses; } public function isCheckedException(string $className, Scope $scope): bool diff --git a/src/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRule.php b/src/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRule.php index 7e4b175e46..c41710b918 100644 --- a/src/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRule.php +++ b/src/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRule.php @@ -8,6 +8,8 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; +use function sprintf; /** * @implements Rule @@ -15,11 +17,8 @@ class MissingCheckedExceptionInFunctionThrowsRule implements Rule { - private MissingCheckedExceptionInThrowsCheck $check; - - public function __construct(MissingCheckedExceptionInThrowsCheck $check) + public function __construct(private MissingCheckedExceptionInThrowsCheck $check) { - $this->check = $check; } public function getNodeType(): string @@ -32,7 +31,7 @@ public function processNode(Node $node, Scope $scope): array $statementResult = $node->getStatementResult(); $functionReflection = $scope->getFunction(); if (!$functionReflection instanceof FunctionReflection) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $errors = []; @@ -40,7 +39,7 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( 'Function %s() throws checked exception %s but it\'s missing from the PHPDoc @throws tag.', $functionReflection->getName(), - $className + $className, )) ->line($throwPointNode->getLine()) ->identifier('exceptions.missingThrowsTag') diff --git a/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php b/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php index e18c0c1d97..be58255d26 100644 --- a/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php +++ b/src/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRule.php @@ -8,6 +8,8 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; +use function sprintf; /** * @implements Rule @@ -15,11 +17,8 @@ class MissingCheckedExceptionInMethodThrowsRule implements Rule { - private MissingCheckedExceptionInThrowsCheck $check; - - public function __construct(MissingCheckedExceptionInThrowsCheck $check) + public function __construct(private MissingCheckedExceptionInThrowsCheck $check) { - $this->check = $check; } public function getNodeType(): string @@ -32,7 +31,7 @@ public function processNode(Node $node, Scope $scope): array $statementResult = $node->getStatementResult(); $methodReflection = $scope->getFunction(); if (!$methodReflection instanceof MethodReflection) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $errors = []; @@ -41,7 +40,7 @@ public function processNode(Node $node, Scope $scope): array 'Method %s::%s() throws checked exception %s but it\'s missing from the PHPDoc @throws tag.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $className + $className, )) ->line($throwPointNode->getLine()) ->identifier('exceptions.missingThrowsTag') diff --git a/src/Rules/Exceptions/MissingCheckedExceptionInThrowsCheck.php b/src/Rules/Exceptions/MissingCheckedExceptionInThrowsCheck.php index 3510d86965..f3dfd9d0fb 100644 --- a/src/Rules/Exceptions/MissingCheckedExceptionInThrowsCheck.php +++ b/src/Rules/Exceptions/MissingCheckedExceptionInThrowsCheck.php @@ -11,19 +11,17 @@ use PHPStan\Type\TypeUtils; use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VerbosityLevel; +use Throwable; +use function array_map; class MissingCheckedExceptionInThrowsCheck { - private ExceptionTypeResolver $exceptionTypeResolver; - - public function __construct(ExceptionTypeResolver $exceptionTypeResolver) + public function __construct(private ExceptionTypeResolver $exceptionTypeResolver) { - $this->exceptionTypeResolver = $exceptionTypeResolver; } /** - * @param Type|null $throwType * @param ThrowPoint[] $throwPoints * @return array */ @@ -40,7 +38,7 @@ public function check(?Type $throwType, array $throwPoints): array } foreach (TypeUtils::flattenTypes($throwPoint->getType()) as $throwPointType) { - if ($throwPointType->isSuperTypeOf(new ObjectType(\Throwable::class))->yes()) { + if ($throwPointType->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) { continue; } if ($throwType->isSuperTypeOf($throwPointType)->yes()) { @@ -74,9 +72,7 @@ private function getNewCatchPosition(Type $throwPointType, Node $throwPointNode) $position = 0; foreach ($tryCatch->catches as $catch) { - $type = TypeCombinator::union(...array_map(static function (Node\Name $class): ObjectType { - return new ObjectType($class->toString()); - }, $catch->types)); + $type = TypeCombinator::union(...array_map(static fn (Node\Name $class): ObjectType => new ObjectType($class->toString()), $catch->types)); if (!$throwPointType->isSuperTypeOf($type)->yes()) { continue; } diff --git a/src/Rules/Exceptions/OverwrittenExitPointByFinallyRule.php b/src/Rules/Exceptions/OverwrittenExitPointByFinallyRule.php index eeb396aa13..4a3b8f5b09 100644 --- a/src/Rules/Exceptions/OverwrittenExitPointByFinallyRule.php +++ b/src/Rules/Exceptions/OverwrittenExitPointByFinallyRule.php @@ -7,6 +7,8 @@ use PHPStan\Node\FinallyExitPointsNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function count; +use function sprintf; /** * @implements Rule diff --git a/src/Rules/Exceptions/ThrowExpressionRule.php b/src/Rules/Exceptions/ThrowExpressionRule.php index fd8999066c..3cd3d68b5a 100644 --- a/src/Rules/Exceptions/ThrowExpressionRule.php +++ b/src/Rules/Exceptions/ThrowExpressionRule.php @@ -14,11 +14,8 @@ class ThrowExpressionRule implements Rule { - private PhpVersion $phpVersion; - - public function __construct(PhpVersion $phpVersion) + public function __construct(private PhpVersion $phpVersion) { - $this->phpVersion = $phpVersion; } public function getNodeType(): string diff --git a/src/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRule.php b/src/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRule.php new file mode 100644 index 0000000000..d0eed19fb9 --- /dev/null +++ b/src/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRule.php @@ -0,0 +1,74 @@ + + */ +class ThrowsVoidFunctionWithExplicitThrowPointRule implements Rule +{ + + public function __construct( + private ExceptionTypeResolver $exceptionTypeResolver, + private bool $missingCheckedExceptionInThrows, + ) + { + } + + public function getNodeType(): string + { + return FunctionReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $statementResult = $node->getStatementResult(); + $functionReflection = $scope->getFunction(); + if (!$functionReflection instanceof FunctionReflection) { + throw new ShouldNotHappenException(); + } + + if (!$functionReflection->getThrowType() instanceof VoidType) { + return []; + } + + $errors = []; + foreach ($statementResult->getThrowPoints() as $throwPoint) { + if (!$throwPoint->isExplicit()) { + continue; + } + + foreach (TypeUtils::flattenTypes($throwPoint->getType()) as $throwPointType) { + if ( + $throwPointType instanceof TypeWithClassName + && $this->exceptionTypeResolver->isCheckedException($throwPointType->getClassName(), $throwPoint->getScope()) + && $this->missingCheckedExceptionInThrows + ) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Function %s() throws exception %s but the PHPDoc contains @throws void.', + $functionReflection->getName(), + $throwPointType->describe(VerbosityLevel::typeOnly()), + ))->line($throwPoint->getNode()->getLine())->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRule.php b/src/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRule.php new file mode 100644 index 0000000000..91c97d157b --- /dev/null +++ b/src/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRule.php @@ -0,0 +1,75 @@ + + */ +class ThrowsVoidMethodWithExplicitThrowPointRule implements Rule +{ + + public function __construct( + private ExceptionTypeResolver $exceptionTypeResolver, + private bool $missingCheckedExceptionInThrows, + ) + { + } + + public function getNodeType(): string + { + return MethodReturnStatementsNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $statementResult = $node->getStatementResult(); + $methodReflection = $scope->getFunction(); + if (!$methodReflection instanceof MethodReflection) { + throw new ShouldNotHappenException(); + } + + if (!$methodReflection->getThrowType() instanceof VoidType) { + return []; + } + + $errors = []; + foreach ($statementResult->getThrowPoints() as $throwPoint) { + if (!$throwPoint->isExplicit()) { + continue; + } + + foreach (TypeUtils::flattenTypes($throwPoint->getType()) as $throwPointType) { + if ( + $throwPointType instanceof TypeWithClassName + && $this->exceptionTypeResolver->isCheckedException($throwPointType->getClassName(), $throwPoint->getScope()) + && $this->missingCheckedExceptionInThrows + ) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Method %s::%s() throws exception %s but the PHPDoc contains @throws void.', + $methodReflection->getDeclaringClass()->getDisplayName(), + $methodReflection->getName(), + $throwPointType->describe(VerbosityLevel::typeOnly()), + ))->line($throwPoint->getNode()->getLine())->build(); + } + } + + return $errors; + } + +} diff --git a/src/Rules/Exceptions/TooWideFunctionThrowTypeRule.php b/src/Rules/Exceptions/TooWideFunctionThrowTypeRule.php index a9f6bab81e..fff550cd41 100644 --- a/src/Rules/Exceptions/TooWideFunctionThrowTypeRule.php +++ b/src/Rules/Exceptions/TooWideFunctionThrowTypeRule.php @@ -8,6 +8,8 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; +use function sprintf; /** * @implements Rule @@ -15,11 +17,8 @@ class TooWideFunctionThrowTypeRule implements Rule { - private TooWideThrowTypeCheck $check; - - public function __construct(TooWideThrowTypeCheck $check) + public function __construct(private TooWideThrowTypeCheck $check) { - $this->check = $check; } public function getNodeType(): string @@ -32,7 +31,7 @@ public function processNode(Node $node, Scope $scope): array $statementResult = $node->getStatementResult(); $functionReflection = $scope->getFunction(); if (!$functionReflection instanceof FunctionReflection) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $throwType = $functionReflection->getThrowType(); @@ -45,7 +44,7 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( 'Function %s() has %s in PHPDoc @throws tag but it\'s not thrown.', $functionReflection->getName(), - $throwClass + $throwClass, )) ->identifier('exceptions.tooWideThrowType') ->metadata([ diff --git a/src/Rules/Exceptions/TooWideMethodThrowTypeRule.php b/src/Rules/Exceptions/TooWideMethodThrowTypeRule.php index c3f388028c..3a26b6843e 100644 --- a/src/Rules/Exceptions/TooWideMethodThrowTypeRule.php +++ b/src/Rules/Exceptions/TooWideMethodThrowTypeRule.php @@ -8,7 +8,9 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; +use function sprintf; /** * @implements Rule @@ -16,14 +18,8 @@ class TooWideMethodThrowTypeRule implements Rule { - private FileTypeMapper $fileTypeMapper; - - private TooWideThrowTypeCheck $check; - - public function __construct(FileTypeMapper $fileTypeMapper, TooWideThrowTypeCheck $check) + public function __construct(private FileTypeMapper $fileTypeMapper, private TooWideThrowTypeCheck $check) { - $this->fileTypeMapper = $fileTypeMapper; - $this->check = $check; } public function getNodeType(): string @@ -36,10 +32,10 @@ public function processNode(Node $node, Scope $scope): array $statementResult = $node->getStatementResult(); $methodReflection = $scope->getFunction(); if (!$methodReflection instanceof MethodReflection) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $docComment = $node->getDocComment(); @@ -53,7 +49,7 @@ public function processNode(Node $node, Scope $scope): array $classReflection->getName(), $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, $methodReflection->getName(), - $docComment->getText() + $docComment->getText(), ); if ($resolvedPhpDoc->getThrowsTag() === null) { @@ -68,7 +64,7 @@ public function processNode(Node $node, Scope $scope): array 'Method %s::%s() has %s in PHPDoc @throws tag but it\'s not thrown.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $throwClass + $throwClass, )) ->identifier('exceptions.tooWideThrowType') ->metadata([ diff --git a/src/Rules/Exceptions/TooWideThrowTypeCheck.php b/src/Rules/Exceptions/TooWideThrowTypeCheck.php index 826b8daad7..59a9ae1d88 100644 --- a/src/Rules/Exceptions/TooWideThrowTypeCheck.php +++ b/src/Rules/Exceptions/TooWideThrowTypeCheck.php @@ -9,12 +9,12 @@ use PHPStan\Type\TypeUtils; use PHPStan\Type\VerbosityLevel; use PHPStan\Type\VoidType; +use function array_map; class TooWideThrowTypeCheck { /** - * @param Type $throwType * @param ThrowPoint[] $throwPoints * @return string[] */ diff --git a/src/Rules/FoundTypeResult.php b/src/Rules/FoundTypeResult.php index b9bd52cbda..919fe2adfd 100644 --- a/src/Rules/FoundTypeResult.php +++ b/src/Rules/FoundTypeResult.php @@ -8,28 +8,17 @@ class FoundTypeResult { - private \PHPStan\Type\Type $type; - - /** @var string[] */ - private array $referencedClasses; - - /** @var RuleError[] */ - private array $unknownClassErrors; - /** - * @param \PHPStan\Type\Type $type * @param string[] $referencedClasses * @param RuleError[] $unknownClassErrors */ public function __construct( - Type $type, - array $referencedClasses, - array $unknownClassErrors + private Type $type, + private array $referencedClasses, + private array $unknownClassErrors, + private ?string $tip, ) { - $this->type = $type; - $this->referencedClasses = $referencedClasses; - $this->unknownClassErrors = $unknownClassErrors; } public function getType(): Type @@ -53,4 +42,9 @@ public function getUnknownClassErrors(): array return $this->unknownClassErrors; } + public function getTip(): ?string + { + return $this->tip; + } + } diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 26d951a98e..f29b301fd3 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -2,12 +2,15 @@ namespace PHPStan\Rules; +use PhpParser\Node; use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; use PHPStan\Php\PhpVersion; +use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ResolvedFunctionVariant; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\NeverType; @@ -17,55 +20,31 @@ use PHPStan\Type\TypeUtils; use PHPStan\Type\VerbosityLevel; use PHPStan\Type\VoidType; +use function array_key_exists; +use function count; +use function is_string; +use function max; +use function sprintf; class FunctionCallParametersCheck { - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - private NullsafeCheck $nullsafeCheck; - - private PhpVersion $phpVersion; - - private UnresolvableTypeHelper $unresolvableTypeHelper; - - private bool $checkArgumentTypes; - - private bool $checkArgumentsPassedByReference; - - private bool $checkExtraArguments; - - private bool $checkMissingTypehints; - - private bool $checkNeverInGenericReturnType; - public function __construct( - RuleLevelHelper $ruleLevelHelper, - NullsafeCheck $nullsafeCheck, - PhpVersion $phpVersion, - UnresolvableTypeHelper $unresolvableTypeHelper, - bool $checkArgumentTypes, - bool $checkArgumentsPassedByReference, - bool $checkExtraArguments, - bool $checkMissingTypehints, - bool $checkNeverInGenericReturnType + private RuleLevelHelper $ruleLevelHelper, + private NullsafeCheck $nullsafeCheck, + private PhpVersion $phpVersion, + private UnresolvableTypeHelper $unresolvableTypeHelper, + private PropertyReflectionFinder $propertyReflectionFinder, + private bool $checkArgumentTypes, + private bool $checkArgumentsPassedByReference, + private bool $checkExtraArguments, + private bool $checkMissingTypehints, ) { - $this->ruleLevelHelper = $ruleLevelHelper; - $this->nullsafeCheck = $nullsafeCheck; - $this->phpVersion = $phpVersion; - $this->unresolvableTypeHelper = $unresolvableTypeHelper; - $this->checkArgumentTypes = $checkArgumentTypes; - $this->checkArgumentsPassedByReference = $checkArgumentsPassedByReference; - $this->checkExtraArguments = $checkExtraArguments; - $this->checkMissingTypehints = $checkMissingTypehints; - $this->checkNeverInGenericReturnType = $checkNeverInGenericReturnType; } /** - * @param \PHPStan\Reflection\ParametersAcceptor $parametersAcceptor - * @param \PHPStan\Analyser\Scope $scope - * @param \PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\New_ $funcCall + * @param Node\Expr\FuncCall|Node\Expr\MethodCall|Node\Expr\StaticCall|Node\Expr\New_ $funcCall * @param array{string, string, string, string, string, string, string, string, string, string, string, string, string} $messages * @return RuleError[] */ @@ -74,7 +53,7 @@ public function check( Scope $scope, bool $isBuiltin, $funcCall, - array $messages + array $messages, ): array { $functionParametersMinCount = 0; @@ -93,8 +72,8 @@ public function check( /** @var array $arguments */ $arguments = []; - /** @var array $args */ - $args = $funcCall->args; + /** @var array $args */ + $args = $funcCall->getArgs(); $hasNamedArguments = false; $hasUnpackedArgument = false; $errors = []; @@ -173,7 +152,7 @@ public function check( ]; } - if ($hasNamedArguments && !$this->phpVersion->supportsNamedArguments()) { + if ($hasNamedArguments && !$this->phpVersion->supportsNamedArguments() && !(bool) $funcCall->getAttribute('isAttribute', false)) { $errors[] = RuleErrorBuilder::message('Named arguments are supported only on PHP 8.0 and later.')->line($funcCall->getLine())->nonIgnorable()->build(); } @@ -194,20 +173,20 @@ public function check( $errors[] = RuleErrorBuilder::message(sprintf( $invokedParametersCount === 1 ? $messages[0] : $messages[1], $invokedParametersCount, - $functionParametersMinCount + $functionParametersMinCount, ))->line($funcCall->getLine())->build(); } elseif ($functionParametersMaxCount === -1 && $invokedParametersCount < $functionParametersMinCount) { $errors[] = RuleErrorBuilder::message(sprintf( $invokedParametersCount === 1 ? $messages[2] : $messages[3], $invokedParametersCount, - $functionParametersMinCount + $functionParametersMinCount, ))->line($funcCall->getLine())->build(); } elseif ($functionParametersMaxCount !== -1) { $errors[] = RuleErrorBuilder::message(sprintf( $invokedParametersCount === 1 ? $messages[4] : $messages[5], $invokedParametersCount, $functionParametersMinCount, - $functionParametersMaxCount + $functionParametersMaxCount, ))->line($funcCall->getLine())->build(); } } @@ -216,7 +195,7 @@ public function check( if ( $scope->getType($funcCall) instanceof VoidType && !$scope->isInFirstLevelStatement() - && !$funcCall instanceof \PhpParser\Node\Expr\New_ + && !$funcCall instanceof Node\Expr\New_ ) { $errors[] = RuleErrorBuilder::message($messages[7])->line($funcCall->getLine())->build(); } @@ -236,9 +215,7 @@ public function check( $scope, $argumentValue, '', - static function (Type $type): bool { - return $type->isIterable()->yes(); - } + static fn (Type $type): bool => $type->isIterable()->yes(), ); $iterableTypeResultType = $iterableTypeResult->getType(); if ( @@ -248,7 +225,7 @@ static function (Type $type): bool { $errors[] = RuleErrorBuilder::message(sprintf( 'Only iterables can be unpacked, %s given in argument #%d.', $iterableTypeResultType->describe(VerbosityLevel::typeOnly()), - $i + 1 + $i + 1, ))->line($argumentLine)->build(); } } @@ -270,10 +247,10 @@ static function (Type $type): bool { $argumentName === null ? sprintf( '#%d %s', $i + 1, - $parameterDescription + $parameterDescription, ) : $parameterDescription, $parameterType->describe($verbosityLevel), - $argumentValueType->describe($verbosityLevel) + $argumentValueType->describe($verbosityLevel), ))->line($argumentLine)->build(); } @@ -288,22 +265,50 @@ static function (Type $type): bool { $parameterDescription = sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName()); $errors[] = RuleErrorBuilder::message(sprintf( $messages[8], - $argumentName === null ? sprintf('#%d %s', $i + 1, $parameterDescription) : $parameterDescription + $argumentName === null ? sprintf('#%d %s', $i + 1, $parameterDescription) : $parameterDescription, ))->line($argumentLine)->build(); continue; } - if ($argumentValue instanceof \PhpParser\Node\Expr\Variable - || $argumentValue instanceof \PhpParser\Node\Expr\ArrayDimFetch - || $argumentValue instanceof \PhpParser\Node\Expr\PropertyFetch - || $argumentValue instanceof \PhpParser\Node\Expr\StaticPropertyFetch) { + if ( + $argumentValue instanceof Node\Expr\PropertyFetch + || $argumentValue instanceof Node\Expr\StaticPropertyFetch) { + $propertyReflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($argumentValue, $scope); + foreach ($propertyReflections as $propertyReflection) { + $nativePropertyReflection = $propertyReflection->getNativeReflection(); + if ($nativePropertyReflection === null) { + continue; + } + if (!$nativePropertyReflection->isReadOnly()) { + continue; + } + + if ($nativePropertyReflection->isStatic()) { + $propertyDescription = sprintf('static readonly property %s::$%s', $propertyReflection->getDeclaringClass()->getDisplayName(), $propertyReflection->getName()); + } else { + $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, + $propertyDescription, + ))->line($argumentLine)->build(); + } + } + + if ($argumentValue instanceof Node\Expr\Variable + || $argumentValue instanceof Node\Expr\ArrayDimFetch + || $argumentValue instanceof Node\Expr\PropertyFetch + || $argumentValue instanceof Node\Expr\StaticPropertyFetch) { continue; } $parameterDescription = sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName()); $errors[] = RuleErrorBuilder::message(sprintf( $messages[8], - $argumentName === null ? sprintf('#%d %s', $i + 1, $parameterDescription) : $parameterDescription + $argumentName === null ? sprintf('#%d %s', $i + 1, $parameterDescription) : $parameterDescription, ))->line($argumentLine)->build(); } @@ -357,8 +362,7 @@ static function (Type $type): bool { } if ( - $this->checkNeverInGenericReturnType - && !$this->unresolvableTypeHelper->containsUnresolvableType($originalParametersAcceptor->getReturnType()) + !$this->unresolvableTypeHelper->containsUnresolvableType($originalParametersAcceptor->getReturnType()) && $this->unresolvableTypeHelper->containsUnresolvableType($parametersAcceptor->getReturnType()) ) { $errors[] = RuleErrorBuilder::message($messages[12])->line($funcCall->getLine())->build(); @@ -369,12 +373,8 @@ static function (Type $type): bool { } /** - * @param ParametersAcceptor $parametersAcceptor * @param array $arguments - * @param bool $hasNamedArguments - * @param string $missingParameterMessage - * @param string $unknownParameterMessage - * @return array{RuleError[], array} + * @return array{RuleError[], array} */ private function processArguments( ParametersAcceptor $parametersAcceptor, @@ -383,7 +383,7 @@ private function processArguments( array $arguments, bool $hasNamedArguments, string $missingParameterMessage, - string $unknownParameterMessage + string $unknownParameterMessage, ): array { $parameters = $parametersAcceptor->getParameters(); diff --git a/src/Rules/FunctionDefinitionCheck.php b/src/Rules/FunctionDefinitionCheck.php index cd920e636b..d311e8f353 100644 --- a/src/Rules/FunctionDefinitionCheck.php +++ b/src/Rules/FunctionDefinitionCheck.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules; +use PhpParser\Node; use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\Variable; use PhpParser\Node\FunctionLike; @@ -19,51 +20,37 @@ use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\NonexistentParentClassType; +use PHPStan\Type\ParserNodeTypeToPHPStanType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; use PHPStan\Type\VerbosityLevel; use PHPStan\Type\VoidType; +use function array_keys; +use function array_map; +use function array_merge; +use function count; +use function is_string; +use function sprintf; class FunctionDefinitionCheck { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private PhpVersion $phpVersion; - - private bool $checkClassCaseSensitivity; - - private bool $checkThisOnly; - - private bool $checkMissingTemplateTypeInParameter; - public function __construct( - ReflectionProvider $reflectionProvider, - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - PhpVersion $phpVersion, - bool $checkClassCaseSensitivity, - bool $checkThisOnly, - bool $checkMissingTemplateTypeInParameter + private ReflectionProvider $reflectionProvider, + private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private UnresolvableTypeHelper $unresolvableTypeHelper, + private PhpVersion $phpVersion, + private bool $checkClassCaseSensitivity, + private bool $checkThisOnly, ) { - $this->reflectionProvider = $reflectionProvider; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->phpVersion = $phpVersion; - $this->checkClassCaseSensitivity = $checkClassCaseSensitivity; - $this->checkThisOnly = $checkThisOnly; - $this->checkMissingTemplateTypeInParameter = $checkMissingTemplateTypeInParameter; } /** - * @param \PhpParser\Node\Stmt\Function_ $function - * @param string $parameterMessage - * @param string $returnMessage - * @param string $unionTypesMessage - * @param string $templateTypeMissingInParameterMessage * @return RuleError[] */ public function checkFunction( @@ -72,7 +59,9 @@ public function checkFunction( string $parameterMessage, string $returnMessage, string $unionTypesMessage, - string $templateTypeMissingInParameterMessage + string $templateTypeMissingInParameterMessage, + string $unresolvableParameterTypeMessage, + string $unresolvableReturnTypeMessage, ): array { $parametersAcceptor = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants()); @@ -83,18 +72,16 @@ public function checkFunction( $parameterMessage, $returnMessage, $unionTypesMessage, - $templateTypeMissingInParameterMessage + $templateTypeMissingInParameterMessage, + $unresolvableParameterTypeMessage, + $unresolvableReturnTypeMessage, ); } /** - * @param \PHPStan\Analyser\Scope $scope - * @param \PhpParser\Node\Param[] $parameters - * @param \PhpParser\Node\Identifier|\PhpParser\Node\Name|\PhpParser\Node\NullableType|\PhpParser\Node\UnionType|null $returnTypeNode - * @param string $parameterMessage - * @param string $returnMessage - * @param string $unionTypesMessage - * @return \PHPStan\Rules\RuleError[] + * @param Node\Param[] $parameters + * @param Node\Identifier|Node\Name|Node\ComplexType|null $returnTypeNode + * @return RuleError[] */ public function checkAnonymousFunction( Scope $scope, @@ -102,7 +89,9 @@ public function checkAnonymousFunction( $returnTypeNode, string $parameterMessage, string $returnMessage, - string $unionTypesMessage + string $unionTypesMessage, + string $unresolvableParameterTypeMessage, + string $unresolvableReturnTypeMessage, ): array { $errors = []; @@ -121,12 +110,19 @@ public function checkAnonymousFunction( } if (!$param->var instanceof Variable || !is_string($param->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $type = $scope->getFunctionType($param->type, false, false); if ($type instanceof VoidType) { $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $param->var->name, 'void'))->line($param->type->getLine())->nonIgnorable()->build(); } + if ( + $this->phpVersion->supportsPureIntersectionTypes() + && $this->unresolvableTypeHelper->containsUnresolvableType($type) + ) { + $errors[] = RuleErrorBuilder::message(sprintf($unresolvableParameterTypeMessage, $param->var->name))->line($param->type->getLine())->nonIgnorable()->build(); + } + foreach ($type->getReferencedClasses() as $class) { if (!$this->reflectionProvider->hasClass($class) || $this->reflectionProvider->getClass($class)->isTrait()) { $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $param->var->name, $class))->line($param->type->getLine())->build(); @@ -135,7 +131,7 @@ public function checkAnonymousFunction( $errors, $this->classCaseSensitivityCheck->checkClassNames([ new ClassNameNodePair($class, $param->type), - ]) + ]), ); } } @@ -158,6 +154,13 @@ public function checkAnonymousFunction( } $returnType = $scope->getFunctionType($returnTypeNode, false, false); + if ( + $this->phpVersion->supportsPureIntersectionTypes() + && $this->unresolvableTypeHelper->containsUnresolvableType($returnType) + ) { + $errors[] = RuleErrorBuilder::message($unresolvableReturnTypeMessage)->line($returnTypeNode->getLine())->nonIgnorable()->build(); + } + foreach ($returnType->getReferencedClasses() as $returnTypeClass) { if (!$this->reflectionProvider->hasClass($returnTypeClass) || $this->reflectionProvider->getClass($returnTypeClass)->isTrait()) { $errors[] = RuleErrorBuilder::message(sprintf($returnMessage, $returnTypeClass))->line($returnTypeNode->getLine())->build(); @@ -166,7 +169,7 @@ public function checkAnonymousFunction( $errors, $this->classCaseSensitivityCheck->checkClassNames([ new ClassNameNodePair($returnTypeClass, $returnTypeNode), - ]) + ]), ); } } @@ -175,12 +178,6 @@ public function checkAnonymousFunction( } /** - * @param PhpMethodFromParserNodeReflection $methodReflection - * @param ClassMethod $methodNode - * @param string $parameterMessage - * @param string $returnMessage - * @param string $unionTypesMessage - * @param string $templateTypeMissingInParameterMessage * @return RuleError[] */ public function checkClassMethod( @@ -189,10 +186,12 @@ public function checkClassMethod( string $parameterMessage, string $returnMessage, string $unionTypesMessage, - string $templateTypeMissingInParameterMessage + string $templateTypeMissingInParameterMessage, + string $unresolvableParameterTypeMessage, + string $unresolvableReturnTypeMessage, ): array { - /** @var \PHPStan\Reflection\ParametersAcceptorWithPhpDocs $parametersAcceptor */ + /** @var ParametersAcceptorWithPhpDocs $parametersAcceptor */ $parametersAcceptor = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants()); return $this->checkParametersAcceptor( @@ -201,17 +200,13 @@ public function checkClassMethod( $parameterMessage, $returnMessage, $unionTypesMessage, - $templateTypeMissingInParameterMessage + $templateTypeMissingInParameterMessage, + $unresolvableParameterTypeMessage, + $unresolvableReturnTypeMessage, ); } /** - * @param ParametersAcceptor $parametersAcceptor - * @param FunctionLike $functionNode - * @param string $parameterMessage - * @param string $returnMessage - * @param string $unionTypesMessage - * @param string $templateTypeMissingInParameterMessage * @return RuleError[] */ private function checkParametersAcceptor( @@ -220,7 +215,9 @@ private function checkParametersAcceptor( string $parameterMessage, string $returnMessage, string $unionTypesMessage, - string $templateTypeMissingInParameterMessage + string $templateTypeMissingInParameterMessage, + string $unresolvableParameterTypeMessage, + string $unresolvableReturnTypeMessage, ): array { $errors = []; @@ -257,15 +254,20 @@ private function checkParametersAcceptor( return $parameterNode; }; - if ( - $parameter instanceof ParameterReflectionWithPhpDocs - && $parameter->getNativeType() instanceof VoidType - ) { + if ($parameter instanceof ParameterReflectionWithPhpDocs) { $parameterVar = $parameterNodeCallback()->var; if (!$parameterVar instanceof Variable || !is_string($parameterVar->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); + } + if ($parameter->getNativeType() instanceof VoidType) { + $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $parameterVar->name, 'void'))->line($parameterNodeCallback()->getLine())->nonIgnorable()->build(); + } + if ( + $this->phpVersion->supportsPureIntersectionTypes() + && $this->unresolvableTypeHelper->containsUnresolvableType($parameter->getNativeType()) + ) { + $errors[] = RuleErrorBuilder::message(sprintf($unresolvableParameterTypeMessage, $parameterVar->name))->line($parameterNodeCallback()->getLine())->nonIgnorable()->build(); } - $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $parameterVar->name, 'void'))->line($parameterNodeCallback()->getLine())->nonIgnorable()->build(); } foreach ($referencedClasses as $class) { if ($this->reflectionProvider->hasClass($class) && !$this->reflectionProvider->getClass($class)->isTrait()) { @@ -275,16 +277,14 @@ private function checkParametersAcceptor( $errors[] = RuleErrorBuilder::message(sprintf( $parameterMessage, $parameter->getName(), - $class + $class, ))->line($parameterNodeCallback()->getLine())->build(); } if ($this->checkClassCaseSensitivity) { $errors = array_merge( $errors, - $this->classCaseSensitivityCheck->checkClassNames(array_map(static function (string $class) use ($parameterNodeCallback): ClassNameNodePair { - return new ClassNameNodePair($class, $parameterNodeCallback()); - }, $referencedClasses)) + $this->classCaseSensitivityCheck->checkClassNames(array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $parameterNodeCallback()), $referencedClasses)), ); } if (!($parameter->getType() instanceof NonexistentParentClassType)) { @@ -294,6 +294,13 @@ private function checkParametersAcceptor( $errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $parameter->getName(), $parameter->getType()->describe(VerbosityLevel::typeOnly())))->line($parameterNodeCallback()->getLine())->build(); } + if ($this->phpVersion->supportsPureIntersectionTypes() && $functionNode->getReturnType() !== null) { + $nativeReturnType = ParserNodeTypeToPHPStanType::resolve($functionNode->getReturnType(), null); + if ($this->unresolvableTypeHelper->containsUnresolvableType($nativeReturnType)) { + $errors[] = RuleErrorBuilder::message($unresolvableReturnTypeMessage)->nonIgnorable()->line($returnTypeNode->getLine())->build(); + } + } + $returnTypeReferencedClasses = $this->getReturnTypeReferencedClasses($parametersAcceptor); foreach ($returnTypeReferencedClasses as $class) { @@ -307,33 +314,29 @@ private function checkParametersAcceptor( if ($this->checkClassCaseSensitivity) { $errors = array_merge( $errors, - $this->classCaseSensitivityCheck->checkClassNames(array_map(static function (string $class) use ($returnTypeNode): ClassNameNodePair { - return new ClassNameNodePair($class, $returnTypeNode); - }, $returnTypeReferencedClasses)) + $this->classCaseSensitivityCheck->checkClassNames(array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $returnTypeNode), $returnTypeReferencedClasses)), ); } if ($parametersAcceptor->getReturnType() instanceof NonexistentParentClassType) { $errors[] = RuleErrorBuilder::message(sprintf($returnMessage, $parametersAcceptor->getReturnType()->describe(VerbosityLevel::typeOnly())))->line($returnTypeNode->getLine())->build(); } - if ($this->checkMissingTemplateTypeInParameter) { - $templateTypeMap = $parametersAcceptor->getTemplateTypeMap(); - $templateTypes = $templateTypeMap->getTypes(); - if (count($templateTypes) > 0) { - foreach ($parametersAcceptor->getParameters() as $parameter) { - TypeTraverser::map($parameter->getType(), static function (Type $type, callable $traverse) use (&$templateTypes): Type { - if ($type instanceof TemplateType) { - unset($templateTypes[$type->getName()]); - return $traverse($type); - } - + $templateTypeMap = $parametersAcceptor->getTemplateTypeMap(); + $templateTypes = $templateTypeMap->getTypes(); + if (count($templateTypes) > 0) { + foreach ($parametersAcceptor->getParameters() as $parameter) { + TypeTraverser::map($parameter->getType(), static function (Type $type, callable $traverse) use (&$templateTypes): Type { + if ($type instanceof TemplateType) { + unset($templateTypes[$type->getName()]); return $traverse($type); - }); - } + } - foreach (array_keys($templateTypes) as $templateTypeName) { - $errors[] = RuleErrorBuilder::message(sprintf($templateTypeMissingInParameterMessage, $templateTypeName))->build(); - } + return $traverse($type); + }); + } + + foreach (array_keys($templateTypes) as $templateTypeName) { + $errors[] = RuleErrorBuilder::message(sprintf($templateTypeMissingInParameterMessage, $templateTypeName))->build(); } } @@ -351,10 +354,10 @@ private function checkRequiredParameterAfterOptional(array $parameterNodes): arr $errors = []; foreach ($parameterNodes as $parameterNode) { if (!$parameterNode->var instanceof Variable) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if (!is_string($parameterNode->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $parameterName = $parameterNode->var->name; if ($optionalParameter !== null && $parameterNode->default === null && !$parameterNode->variadic) { @@ -387,17 +390,15 @@ private function checkRequiredParameterAfterOptional(array $parameterNodes): arr } /** - * @param string $parameterName * @param Param[] $parameterNodes - * @return Param */ private function getParameterNode( string $parameterName, - array $parameterNodes + array $parameterNodes, ): Param { foreach ($parameterNodes as $param) { - if ($param->var instanceof \PhpParser\Node\Expr\Error) { + if ($param->var instanceof Node\Expr\Error) { continue; } @@ -410,11 +411,10 @@ private function getParameterNode( } } - throw new \PHPStan\ShouldNotHappenException(sprintf('Parameter %s not found.', $parameterName)); + throw new ShouldNotHappenException(sprintf('Parameter %s not found.', $parameterName)); } /** - * @param \PHPStan\Reflection\ParameterReflection $parameter * @return string[] */ private function getParameterReferencedClasses(ParameterReflection $parameter): array @@ -429,12 +429,11 @@ private function getParameterReferencedClasses(ParameterReflection $parameter): return array_merge( $parameter->getNativeType()->getReferencedClasses(), - $parameter->getPhpDocType()->getReferencedClasses() + $parameter->getPhpDocType()->getReferencedClasses(), ); } /** - * @param \PHPStan\Reflection\ParametersAcceptor $parametersAcceptor * @return string[] */ private function getReturnTypeReferencedClasses(ParametersAcceptor $parametersAcceptor): array @@ -449,7 +448,7 @@ private function getReturnTypeReferencedClasses(ParametersAcceptor $parametersAc return array_merge( $parametersAcceptor->getNativeReturnType()->getReferencedClasses(), - $parametersAcceptor->getPhpDocReturnType()->getReferencedClasses() + $parametersAcceptor->getPhpDocReturnType()->getReferencedClasses(), ); } diff --git a/src/Rules/FunctionReturnTypeCheck.php b/src/Rules/FunctionReturnTypeCheck.php index f3ee8d568b..20995fbc97 100644 --- a/src/Rules/FunctionReturnTypeCheck.php +++ b/src/Rules/FunctionReturnTypeCheck.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules; +use Generator; use PhpParser\Node; use PhpParser\Node\Expr; use PHPStan\Analyser\Scope; @@ -11,25 +12,16 @@ use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VerbosityLevel; use PHPStan\Type\VoidType; +use function sprintf; class FunctionReturnTypeCheck { - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - public function __construct(RuleLevelHelper $ruleLevelHelper) + public function __construct(private RuleLevelHelper $ruleLevelHelper) { - $this->ruleLevelHelper = $ruleLevelHelper; } /** - * @param \PHPStan\Analyser\Scope $scope - * @param \PHPStan\Type\Type $returnType - * @param \PhpParser\Node\Expr|null $returnValue - * @param string $emptyReturnStatementMessage - * @param string $voidMessage - * @param string $typeMismatchMessage - * @param bool $isGenerator * @return RuleError[] */ public function checkReturnType( @@ -41,7 +33,7 @@ public function checkReturnType( string $voidMessage, string $typeMismatchMessage, string $neverMessage, - bool $isGenerator + bool $isGenerator, ): array { if ($returnType instanceof NeverType && $returnType->isExplicit()) { @@ -59,8 +51,8 @@ public function checkReturnType( $returnType = GenericTypeVariableResolver::getType( $returnType, - \Generator::class, - 'TReturn' + Generator::class, + 'TReturn', ); if ($returnType === null) { return []; @@ -77,7 +69,7 @@ public function checkReturnType( return [ RuleErrorBuilder::message(sprintf( $emptyReturnStatementMessage, - $returnType->describe($verbosityLevel) + $returnType->describe($verbosityLevel), ))->line($returnNode->getLine())->build(), ]; } @@ -89,7 +81,7 @@ public function checkReturnType( return [ RuleErrorBuilder::message(sprintf( $voidMessage, - $returnValueType->describe($verbosityLevel) + $returnValueType->describe($verbosityLevel), ))->line($returnNode->getLine())->build(), ]; } @@ -99,7 +91,7 @@ public function checkReturnType( RuleErrorBuilder::message(sprintf( $typeMismatchMessage, $returnType->describe($verbosityLevel), - $returnValueType->describe($verbosityLevel) + $returnValueType->describe($verbosityLevel), ))->line($returnNode->getLine())->build(), ]; } diff --git a/src/Rules/Functions/ArrowFunctionAttributesRule.php b/src/Rules/Functions/ArrowFunctionAttributesRule.php index b45c47f3c3..1b4e1ddf03 100644 --- a/src/Rules/Functions/ArrowFunctionAttributesRule.php +++ b/src/Rules/Functions/ArrowFunctionAttributesRule.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Functions; +use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Rules\AttributesCheck; @@ -13,11 +14,8 @@ class ArrowFunctionAttributesRule implements Rule { - private AttributesCheck $attributesCheck; - - public function __construct(AttributesCheck $attributesCheck) + public function __construct(private AttributesCheck $attributesCheck) { - $this->attributesCheck = $attributesCheck; } public function getNodeType(): string @@ -30,8 +28,8 @@ public function processNode(Node $node, Scope $scope): array return $this->attributesCheck->check( $scope, $node->attrGroups, - \Attribute::TARGET_FUNCTION, - 'function' + Attribute::TARGET_FUNCTION, + 'function', ); } diff --git a/src/Rules/Functions/ArrowFunctionReturnNullsafeByRefRule.php b/src/Rules/Functions/ArrowFunctionReturnNullsafeByRefRule.php index 0dac5917a3..3606f67a71 100644 --- a/src/Rules/Functions/ArrowFunctionReturnNullsafeByRefRule.php +++ b/src/Rules/Functions/ArrowFunctionReturnNullsafeByRefRule.php @@ -14,11 +14,8 @@ class ArrowFunctionReturnNullsafeByRefRule implements Rule { - private NullsafeCheck $nullsafeCheck; - - public function __construct(NullsafeCheck $nullsafeCheck) + public function __construct(private NullsafeCheck $nullsafeCheck) { - $this->nullsafeCheck = $nullsafeCheck; } public function getNodeType(): string diff --git a/src/Rules/Functions/ArrowFunctionReturnTypeRule.php b/src/Rules/Functions/ArrowFunctionReturnTypeRule.php index 0a4cd265ad..86bd340d54 100644 --- a/src/Rules/Functions/ArrowFunctionReturnTypeRule.php +++ b/src/Rules/Functions/ArrowFunctionReturnTypeRule.php @@ -2,24 +2,25 @@ namespace PHPStan\Rules\Functions; +use Generator; use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\InArrowFunctionNode; use PHPStan\Rules\FunctionReturnTypeCheck; +use PHPStan\Rules\Rule; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\ObjectType; +use PHPStan\Type\Type; use PHPStan\Type\VoidType; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\InArrowFunctionNode> + * @implements Rule */ -class ArrowFunctionReturnTypeRule implements \PHPStan\Rules\Rule +class ArrowFunctionReturnTypeRule implements Rule { - private \PHPStan\Rules\FunctionReturnTypeCheck $returnTypeCheck; - - public function __construct(FunctionReturnTypeCheck $returnTypeCheck) + public function __construct(private FunctionReturnTypeCheck $returnTypeCheck) { - $this->returnTypeCheck = $returnTypeCheck; } public function getNodeType(): string @@ -30,12 +31,12 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { if (!$scope->isInAnonymousFunction()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - /** @var \PHPStan\Type\Type $returnType */ + /** @var Type $returnType */ $returnType = $scope->getAnonymousFunctionReturnType(); - $generatorType = new ObjectType(\Generator::class); + $generatorType = new ObjectType(Generator::class); $originalNode = $node->getOriginalNode(); $isVoidSuperType = (new VoidType())->isSuperTypeOf($returnType); @@ -52,7 +53,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.', - $generatorType->isSuperTypeOf($returnType)->yes() + $generatorType->isSuperTypeOf($returnType)->yes(), ); } diff --git a/src/Rules/Functions/CallCallablesRule.php b/src/Rules/Functions/CallCallablesRule.php index 5793b79219..0873fdc892 100644 --- a/src/Rules/Functions/CallCallablesRule.php +++ b/src/Rules/Functions/CallCallablesRule.php @@ -2,61 +2,58 @@ namespace PHPStan\Rules\Functions; +use PhpParser\Node; +use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\InaccessibleMethod; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\FunctionCallParametersCheck; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ClosureType; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function array_merge; +use function count; +use function sprintf; +use function ucfirst; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\FuncCall> + * @implements Rule */ -class CallCallablesRule implements \PHPStan\Rules\Rule +class CallCallablesRule implements Rule { - private \PHPStan\Rules\FunctionCallParametersCheck $check; - - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - private bool $reportMaybes; - public function __construct( - FunctionCallParametersCheck $check, - RuleLevelHelper $ruleLevelHelper, - bool $reportMaybes + private FunctionCallParametersCheck $check, + private RuleLevelHelper $ruleLevelHelper, + private bool $reportMaybes, ) { - $this->check = $check; - $this->ruleLevelHelper = $ruleLevelHelper; - $this->reportMaybes = $reportMaybes; } public function getNodeType(): string { - return \PhpParser\Node\Expr\FuncCall::class; + return Node\Expr\FuncCall::class; } public function processNode( - \PhpParser\Node $node, - Scope $scope + Node $node, + Scope $scope, ): array { - if (!$node->name instanceof \PhpParser\Node\Expr) { + if (!$node->name instanceof Node\Expr) { return []; } $typeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, - $node->name, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->name), 'Invoking callable on an unknown class %s.', - static function (Type $type): bool { - return $type->isCallable()->yes(); - } + static fn (Type $type): bool => $type->isCallable()->yes(), ); $type = $typeResult->getType(); if ($type instanceof ErrorType) { @@ -67,14 +64,14 @@ static function (Type $type): bool { if ($isCallable->no()) { return [ RuleErrorBuilder::message( - sprintf('Trying to invoke %s but it\'s not a callable.', $type->describe(VerbosityLevel::value())) + sprintf('Trying to invoke %s but it\'s not a callable.', $type->describe(VerbosityLevel::value())), )->build(), ]; } if ($this->reportMaybes && $isCallable->maybe()) { return [ RuleErrorBuilder::message( - sprintf('Trying to invoke %s but it might not be a callable.', $type->describe(VerbosityLevel::value())) + sprintf('Trying to invoke %s but it might not be a callable.', $type->describe(VerbosityLevel::value())), )->build(), ]; } @@ -91,20 +88,20 @@ static function (Type $type): bool { 'Call to %s method %s() of class %s.', $method->isPrivate() ? 'private' : 'protected', $method->getName(), - $method->getDeclaringClass()->getDisplayName() + $method->getDeclaringClass()->getDisplayName(), ))->build(); } $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $scope, - $node->args, - $parametersAcceptors + $node->getArgs(), + $parametersAcceptors, ); if ($type instanceof ClosureType) { $callableDescription = 'closure'; } else { - $callableDescription = sprintf('callable %s', $type->describe(VerbosityLevel::value())); + $callableDescription = sprintf('callable %s', SprintfHelper::escapeFormatString($type->describe(VerbosityLevel::value()))); } return array_merge( @@ -128,8 +125,8 @@ static function (Type $type): bool { 'Missing parameter $%s in call to ' . $callableDescription . '.', 'Unknown parameter $%s in call to ' . $callableDescription . '.', 'Return type of call to ' . $callableDescription . ' contains unresolvable type.', - ] - ) + ], + ), ); } diff --git a/src/Rules/Functions/CallToFunctionParametersRule.php b/src/Rules/Functions/CallToFunctionParametersRule.php index 927f9f3d88..71dd7e06e2 100644 --- a/src/Rules/Functions/CallToFunctionParametersRule.php +++ b/src/Rules/Functions/CallToFunctionParametersRule.php @@ -5,24 +5,20 @@ use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\FunctionCallParametersCheck; +use PHPStan\Rules\Rule; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\FuncCall> + * @implements Rule */ -class CallToFunctionParametersRule implements \PHPStan\Rules\Rule +class CallToFunctionParametersRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\FunctionCallParametersCheck $check; - - public function __construct(ReflectionProvider $reflectionProvider, FunctionCallParametersCheck $check) + public function __construct(private ReflectionProvider $reflectionProvider, private FunctionCallParametersCheck $check) { - $this->reflectionProvider = $reflectionProvider; - $this->check = $check; } public function getNodeType(): string @@ -32,7 +28,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!($node->name instanceof \PhpParser\Node\Name)) { + if (!($node->name instanceof Node\Name)) { return []; } @@ -41,31 +37,32 @@ public function processNode(Node $node, Scope $scope): array } $function = $this->reflectionProvider->getFunction($node->name, $scope); + $functionName = SprintfHelper::escapeFormatString($function->getName()); return $this->check->check( ParametersAcceptorSelector::selectFromArgs( $scope, - $node->args, - $function->getVariants() + $node->getArgs(), + $function->getVariants(), ), $scope, $function->isBuiltin(), $node, [ - 'Function ' . $function->getName() . ' invoked with %d parameter, %d required.', - 'Function ' . $function->getName() . ' invoked with %d parameters, %d required.', - 'Function ' . $function->getName() . ' invoked with %d parameter, at least %d required.', - 'Function ' . $function->getName() . ' invoked with %d parameters, at least %d required.', - 'Function ' . $function->getName() . ' invoked with %d parameter, %d-%d required.', - 'Function ' . $function->getName() . ' invoked with %d parameters, %d-%d required.', - 'Parameter %s of function ' . $function->getName() . ' expects %s, %s given.', - 'Result of function ' . $function->getName() . ' (void) is used.', - 'Parameter %s of function ' . $function->getName() . ' is passed by reference, so it expects variables only.', - 'Unable to resolve the template type %s in call to function ' . $function->getName(), - 'Missing parameter $%s in call to function ' . $function->getName() . '.', - 'Unknown parameter $%s in call to function ' . $function->getName() . '.', - 'Return type of call to function ' . $function->getName() . ' contains unresolvable type.', - ] + 'Function ' . $functionName . ' invoked with %d parameter, %d required.', + 'Function ' . $functionName . ' invoked with %d parameters, %d required.', + 'Function ' . $functionName . ' invoked with %d parameter, at least %d required.', + 'Function ' . $functionName . ' invoked with %d parameters, at least %d required.', + 'Function ' . $functionName . ' invoked with %d parameter, %d-%d required.', + 'Function ' . $functionName . ' invoked with %d parameters, %d-%d required.', + 'Parameter %s of function ' . $functionName . ' expects %s, %s given.', + 'Result of function ' . $functionName . ' (void) is used.', + 'Parameter %s of function ' . $functionName . ' is passed by reference, so it expects variables only.', + 'Unable to resolve the template type %s in call to function ' . $functionName, + 'Missing parameter $%s in call to function ' . $functionName . '.', + 'Unknown parameter $%s in call to function ' . $functionName . '.', + 'Return type of call to function ' . $functionName . ' contains unresolvable type.', + ], ); } diff --git a/src/Rules/Functions/CallToFunctionStamentWithoutSideEffectsRule.php b/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php similarity index 68% rename from src/Rules/Functions/CallToFunctionStamentWithoutSideEffectsRule.php rename to src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php index b38bc1feaa..32b0fb7d77 100644 --- a/src/Rules/Functions/CallToFunctionStamentWithoutSideEffectsRule.php +++ b/src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php @@ -9,18 +9,17 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\NeverType; use PHPStan\Type\VoidType; +use function in_array; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Expression> + * @implements Rule */ -class CallToFunctionStamentWithoutSideEffectsRule implements Rule +class CallToFunctionStatementWithoutSideEffectsRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct(private ReflectionProvider $reflectionProvider) { - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -35,7 +34,7 @@ public function processNode(Node $node, Scope $scope): array } $funcCall = $node->expr; - if (!($funcCall->name instanceof \PhpParser\Node\Name)) { + if (!($funcCall->name instanceof Node\Name)) { return []; } @@ -44,10 +43,12 @@ public function processNode(Node $node, Scope $scope): array } $function = $this->reflectionProvider->getFunction($funcCall->name, $scope); - if ($function->hasSideEffects()->no()) { - $throwsType = $function->getThrowType(); - if ($throwsType !== null && !$throwsType instanceof VoidType) { - return []; + if ($function->hasSideEffects()->no() || $node->expr->isFirstClassCallable()) { + if (!$node->expr->isFirstClassCallable()) { + $throwsType = $function->getThrowType(); + if ($throwsType !== null && !$throwsType instanceof VoidType) { + return []; + } } $functionResult = $scope->getType($funcCall); @@ -66,7 +67,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message(sprintf( 'Call to function %s() on a separate line has no effect.', - $function->getName() + $function->getName(), ))->build(), ]; } diff --git a/src/Rules/Functions/CallToNonExistentFunctionRule.php b/src/Rules/Functions/CallToNonExistentFunctionRule.php index b4a10083f2..7fc4443838 100644 --- a/src/Rules/Functions/CallToNonExistentFunctionRule.php +++ b/src/Rules/Functions/CallToNonExistentFunctionRule.php @@ -6,25 +6,22 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function sprintf; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\FuncCall> + * @implements Rule */ -class CallToNonExistentFunctionRule implements \PHPStan\Rules\Rule +class CallToNonExistentFunctionRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private bool $checkFunctionNameCase; - public function __construct( - ReflectionProvider $reflectionProvider, - bool $checkFunctionNameCase + private ReflectionProvider $reflectionProvider, + private bool $checkFunctionNameCase, ) { - $this->reflectionProvider = $reflectionProvider; - $this->checkFunctionNameCase = $checkFunctionNameCase; } public function getNodeType(): string @@ -34,11 +31,15 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!($node->name instanceof \PhpParser\Node\Name)) { + if (!($node->name instanceof Node\Name)) { return []; } if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + if ($scope->isInFunctionExists($node->name->toString())) { + return []; + } + return [ RuleErrorBuilder::message(sprintf('Function %s not found.', (string) $node->name))->discoveringSymbolsTip()->build(), ]; @@ -58,7 +59,7 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'Call to function %s() with incorrect case: %s', $function->getName(), - $name + $name, ))->build(), ]; } diff --git a/src/Rules/Functions/ClosureAttributesRule.php b/src/Rules/Functions/ClosureAttributesRule.php index 2048c09a8b..170d9ad05e 100644 --- a/src/Rules/Functions/ClosureAttributesRule.php +++ b/src/Rules/Functions/ClosureAttributesRule.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Functions; +use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Rules\AttributesCheck; @@ -13,11 +14,8 @@ class ClosureAttributesRule implements Rule { - private AttributesCheck $attributesCheck; - - public function __construct(AttributesCheck $attributesCheck) + public function __construct(private AttributesCheck $attributesCheck) { - $this->attributesCheck = $attributesCheck; } public function getNodeType(): string @@ -30,8 +28,8 @@ public function processNode(Node $node, Scope $scope): array return $this->attributesCheck->check( $scope, $node->attrGroups, - \Attribute::TARGET_FUNCTION, - 'function' + Attribute::TARGET_FUNCTION, + 'function', ); } diff --git a/src/Rules/Functions/ClosureReturnTypeRule.php b/src/Rules/Functions/ClosureReturnTypeRule.php index d1dac5ad77..22603a10c7 100644 --- a/src/Rules/Functions/ClosureReturnTypeRule.php +++ b/src/Rules/Functions/ClosureReturnTypeRule.php @@ -6,19 +6,19 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\ClosureReturnStatementsNode; use PHPStan\Rules\FunctionReturnTypeCheck; +use PHPStan\Rules\Rule; +use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function count; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -class ClosureReturnTypeRule implements \PHPStan\Rules\Rule +class ClosureReturnTypeRule implements Rule { - private \PHPStan\Rules\FunctionReturnTypeCheck $returnTypeCheck; - - public function __construct(FunctionReturnTypeCheck $returnTypeCheck) + public function __construct(private FunctionReturnTypeCheck $returnTypeCheck) { - $this->returnTypeCheck = $returnTypeCheck; } public function getNodeType(): string @@ -32,7 +32,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - /** @var \PHPStan\Type\Type $returnType */ + /** @var Type $returnType */ $returnType = $scope->getAnonymousFunctionReturnType(); $containsNull = TypeCombinator::containsNull($returnType); $hasNativeTypehint = $node->getClosureExpr()->returnType !== null; @@ -53,7 +53,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 + count($node->getYieldStatements()) > 0, ); foreach ($returnMessages as $returnMessage) { diff --git a/src/Rules/Functions/ClosureUsesThisRule.php b/src/Rules/Functions/ClosureUsesThisRule.php deleted file mode 100644 index 60018f454b..0000000000 --- a/src/Rules/Functions/ClosureUsesThisRule.php +++ /dev/null @@ -1,45 +0,0 @@ - - */ -class ClosureUsesThisRule implements Rule -{ - - public function getNodeType(): string - { - return Node\Expr\Closure::class; - } - - public function processNode(Node $node, Scope $scope): array - { - if ($node->static) { - return []; - } - - $messages = []; - foreach ($node->uses as $closureUse) { - $varType = $scope->getType($closureUse->var); - if (!is_string($closureUse->var->name)) { - continue; - } - if (!$varType instanceof ThisType) { - continue; - } - - $messages[] = RuleErrorBuilder::message(sprintf('Anonymous function uses $this assigned to variable $%s. Use $this directly in the function body.', $closureUse->var->name)) - ->line($closureUse->getLine()) - ->build(); - } - return $messages; - } - -} diff --git a/src/Rules/Functions/DefineParametersRule.php b/src/Rules/Functions/DefineParametersRule.php new file mode 100644 index 0000000000..ec5a2590b3 --- /dev/null +++ b/src/Rules/Functions/DefineParametersRule.php @@ -0,0 +1,54 @@ + + */ +class DefineParametersRule implements Rule +{ + + public function __construct(private PhpVersion $phpVersion) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof Node\Name)) { + return []; + } + if ($this->phpVersion->supportsCaseInsensitiveConstantNames()) { + return []; + } + $name = strtolower((string) $node->name); + if ($name !== 'define') { + return []; + } + $args = $node->getArgs(); + $argsCount = count($args); + // Expects 2 or 3, 1 arg is caught by CallToFunctionParametersRule + if ($argsCount < 3) { + return []; + } + return [ + RuleErrorBuilder::message( + 'Argument #3 ($case_insensitive) is ignored since declaration of case-insensitive constants is no longer supported.', + )->line($node->getLine())->build(), + ]; + } + +} diff --git a/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php b/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php index 8b5e4a304f..ecaf404573 100644 --- a/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php +++ b/src/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRule.php @@ -5,18 +5,16 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Rules\FunctionDefinitionCheck; +use PHPStan\Rules\Rule; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\ArrowFunction> + * @implements Rule */ -class ExistingClassesInArrowFunctionTypehintsRule implements \PHPStan\Rules\Rule +class ExistingClassesInArrowFunctionTypehintsRule implements Rule { - private \PHPStan\Rules\FunctionDefinitionCheck $check; - - public function __construct(FunctionDefinitionCheck $check) + public function __construct(private FunctionDefinitionCheck $check) { - $this->check = $check; } public function getNodeType(): string @@ -30,9 +28,11 @@ public function processNode(Node $node, Scope $scope): array $scope, $node->getParams(), $node->getReturnType(), - 'Parameter $%s of anonymous function has invalid typehint type %s.', - 'Return typehint of anonymous function has invalid type %s.', - 'Anonymous function uses native union types but they\'re supported only on PHP 8.0 and later.' + 'Parameter $%s of anonymous function has invalid type %s.', + 'Anonymous function has invalid return type %s.', + '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/ExistingClassesInClosureTypehintsRule.php b/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php index 3b2d45f799..2c6dbd3ad0 100644 --- a/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php +++ b/src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php @@ -6,18 +6,16 @@ use PhpParser\Node\Expr\Closure; use PHPStan\Analyser\Scope; use PHPStan\Rules\FunctionDefinitionCheck; +use PHPStan\Rules\Rule; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Closure> + * @implements Rule */ -class ExistingClassesInClosureTypehintsRule implements \PHPStan\Rules\Rule +class ExistingClassesInClosureTypehintsRule implements Rule { - private \PHPStan\Rules\FunctionDefinitionCheck $check; - - public function __construct(FunctionDefinitionCheck $check) + public function __construct(private FunctionDefinitionCheck $check) { - $this->check = $check; } public function getNodeType(): string @@ -31,9 +29,11 @@ public function processNode(Node $node, Scope $scope): array $scope, $node->getParams(), $node->getReturnType(), - 'Parameter $%s of anonymous function has invalid typehint type %s.', - 'Return typehint of anonymous function has invalid type %s.', - 'Anonymous function uses native union types but they\'re supported only on PHP 8.0 and later.' + 'Parameter $%s of anonymous function has invalid type %s.', + 'Anonymous function has invalid return type %s.', + 'Anonymous function uses native union types but they\'re supported only on PHP 8.0 and later.', + 'Parameter $%s of anonymous function has unresolvable native type.', + 'Anonymous function has unresolvable native return type.', ); } diff --git a/src/Rules/Functions/ExistingClassesInTypehintsRule.php b/src/Rules/Functions/ExistingClassesInTypehintsRule.php index c03593ec9c..4314e6f69d 100644 --- a/src/Rules/Functions/ExistingClassesInTypehintsRule.php +++ b/src/Rules/Functions/ExistingClassesInTypehintsRule.php @@ -4,21 +4,21 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InFunctionNode; use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; use PHPStan\Rules\FunctionDefinitionCheck; +use PHPStan\Rules\Rule; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -class ExistingClassesInTypehintsRule implements \PHPStan\Rules\Rule +class ExistingClassesInTypehintsRule implements Rule { - private \PHPStan\Rules\FunctionDefinitionCheck $check; - - public function __construct(FunctionDefinitionCheck $check) + public function __construct(private FunctionDefinitionCheck $check) { - $this->check = $check; } public function getNodeType(): string @@ -32,21 +32,29 @@ public function processNode(Node $node, Scope $scope): array return []; } - $functionName = $scope->getFunction()->getName(); + $functionName = SprintfHelper::escapeFormatString($scope->getFunction()->getName()); return $this->check->checkFunction( $node->getOriginalNode(), $scope->getFunction(), sprintf( - 'Parameter $%%s of function %s() has invalid typehint type %%s.', - $functionName + 'Parameter $%%s of function %s() has invalid type %%s.', + $functionName, ), sprintf( - 'Return typehint of function %s() has invalid type %%s.', - $functionName + 'Function %s() has invalid return type %%s.', + $functionName, ), sprintf('Function %s() uses native union types but they\'re supported only on PHP 8.0 and later.', $functionName), - sprintf('Template type %%s of function %s() is not referenced in a parameter.', $functionName) + sprintf('Template type %%s of function %s() is not referenced in a parameter.', $functionName), + sprintf( + 'Parameter $%%s of function %s() has unresolvable native type.', + $functionName, + ), + sprintf( + 'Function %s() has unresolvable native return type.', + $functionName, + ), ); } diff --git a/src/Rules/Functions/FunctionAttributesRule.php b/src/Rules/Functions/FunctionAttributesRule.php index 758d779b47..2ccf122ac2 100644 --- a/src/Rules/Functions/FunctionAttributesRule.php +++ b/src/Rules/Functions/FunctionAttributesRule.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Functions; +use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Rules\AttributesCheck; @@ -13,11 +14,8 @@ class FunctionAttributesRule implements Rule { - private AttributesCheck $attributesCheck; - - public function __construct(AttributesCheck $attributesCheck) + public function __construct(private AttributesCheck $attributesCheck) { - $this->attributesCheck = $attributesCheck; } public function getNodeType(): string @@ -30,8 +28,8 @@ public function processNode(Node $node, Scope $scope): array return $this->attributesCheck->check( $scope, $node->attrGroups, - \Attribute::TARGET_FUNCTION, - 'function' + Attribute::TARGET_FUNCTION, + 'function', ); } diff --git a/src/Rules/Functions/FunctionCallableRule.php b/src/Rules/Functions/FunctionCallableRule.php new file mode 100644 index 0000000000..5905fc6dec --- /dev/null +++ b/src/Rules/Functions/FunctionCallableRule.php @@ -0,0 +1,111 @@ + + */ +class FunctionCallableRule implements Rule +{ + + public function __construct(private ReflectionProvider $reflectionProvider, private RuleLevelHelper $ruleLevelHelper, private PhpVersion $phpVersion, private bool $checkFunctionNameCase, private bool $reportMaybes) + { + } + + public function getNodeType(): string + { + return FunctionCallableNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$this->phpVersion->supportsFirstClassCallables()) { + return [ + RuleErrorBuilder::message('First-class callables are supported only on PHP 8.1 and later.') + ->nonIgnorable() + ->build(), + ]; + } + + $functionName = $node->getName(); + if ($functionName instanceof Node\Name) { + $functionNameName = $functionName->toString(); + if ($this->reflectionProvider->hasFunction($functionName, $scope)) { + if ($this->checkFunctionNameCase) { + $function = $this->reflectionProvider->getFunction($functionName, $scope); + + /** @var string $calledFunctionName */ + $calledFunctionName = $this->reflectionProvider->resolveFunctionName($functionName, $scope); + if ( + strtolower($function->getName()) === strtolower($calledFunctionName) + && $function->getName() !== $calledFunctionName + ) { + return [ + RuleErrorBuilder::message(sprintf( + 'Call to function %s() with incorrect case: %s', + $function->getName(), + $functionNameName, + ))->build(), + ]; + } + } + + return []; + } + + if ($scope->isInFunctionExists($functionNameName)) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf('Function %s not found.', $functionNameName)) + ->build(), + ]; + } + + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $functionName), + 'Creating callable from an unknown class %s.', + static fn (Type $type): bool => $type->isCallable()->yes(), + ); + $type = $typeResult->getType(); + if ($type instanceof ErrorType) { + return $typeResult->getUnknownClassErrors(); + } + + $isCallable = $type->isCallable(); + if ($isCallable->no()) { + return [ + RuleErrorBuilder::message( + sprintf('Creating callable from %s but it\'s not a callable.', $type->describe(VerbosityLevel::value())), + )->build(), + ]; + } + if ($this->reportMaybes && $isCallable->maybe()) { + return [ + RuleErrorBuilder::message( + sprintf('Creating callable from %s but it might not be a callable.', $type->describe(VerbosityLevel::value())), + )->build(), + ]; + } + + return []; + } + +} diff --git a/src/Rules/Functions/ImplodeFunctionRule.php b/src/Rules/Functions/ImplodeFunctionRule.php new file mode 100644 index 0000000000..3d75e6d789 --- /dev/null +++ b/src/Rules/Functions/ImplodeFunctionRule.php @@ -0,0 +1,78 @@ + + */ +class ImplodeFunctionRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + private RuleLevelHelper $ruleLevelHelper, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof Node\Name)) { + return []; + } + + $functionName = $this->reflectionProvider->resolveFunctionName($node->name, $scope); + if (!in_array($functionName, ['implode', 'join'], true)) { + return []; + } + + $args = $node->getArgs(); + if (count($args) === 1) { + $arrayArg = $args[0]->value; + $paramNo = 1; + } elseif (count($args) === 2) { + $arrayArg = $args[1]->value; + $paramNo = 2; + } else { + return []; + } + + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $arrayArg, + '', + static fn (Type $type): bool => !$type->getIterableValueType()->toString() instanceof ErrorType, + ); + + if ($typeResult->getType() instanceof ErrorType + || !$typeResult->getType()->getIterableValueType()->toString() instanceof ErrorType) { + return []; + } + + return [ + RuleErrorBuilder::message( + sprintf('Parameter #%d $array of function %s expects array, %s given.', $paramNo, $functionName, $typeResult->getType()->describe(VerbosityLevel::typeOnly())), + )->build(), + ]; + } + +} diff --git a/src/Rules/Functions/IncompatibleDefaultParameterTypeRule.php b/src/Rules/Functions/IncompatibleDefaultParameterTypeRule.php index 031a4b9c31..4d31ea77a4 100644 --- a/src/Rules/Functions/IncompatibleDefaultParameterTypeRule.php +++ b/src/Rules/Functions/IncompatibleDefaultParameterTypeRule.php @@ -9,11 +9,14 @@ use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\VerbosityLevel; +use function is_string; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\InFunctionNode> + * @implements Rule */ class IncompatibleDefaultParameterTypeRule implements Rule { @@ -40,7 +43,7 @@ public function processNode(Node $node, Scope $scope): array $param->var instanceof Node\Expr\Error || !is_string($param->var->name) ) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $defaultValueType = $scope->getType($param->default); @@ -59,7 +62,7 @@ public function processNode(Node $node, Scope $scope): array $param->var->name, $defaultValueType->describe($verbosityLevel), $function->getName(), - $parameterType->describe($verbosityLevel) + $parameterType->describe($verbosityLevel), ))->line($param->getLine())->build(); } diff --git a/src/Rules/Functions/InnerFunctionRule.php b/src/Rules/Functions/InnerFunctionRule.php index 5d0c0cfe93..9c58ea233a 100644 --- a/src/Rules/Functions/InnerFunctionRule.php +++ b/src/Rules/Functions/InnerFunctionRule.php @@ -5,12 +5,13 @@ use PhpParser\Node; use PhpParser\Node\Stmt\Function_; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Function_> + * @implements Rule */ -class InnerFunctionRule implements \PHPStan\Rules\Rule +class InnerFunctionRule implements Rule { public function getNodeType(): string @@ -26,7 +27,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message( - 'Inner named functions are not supported by PHPStan. Consider refactoring to an anonymous function, class method, or a top-level-defined function. See issue #165 (https://github.com/phpstan/phpstan/issues/165) for more details.' + 'Inner named functions are not supported by PHPStan. Consider refactoring to an anonymous function, class method, or a top-level-defined function. See issue #165 (https://github.com/phpstan/phpstan/issues/165) for more details.', )->build(), ]; } diff --git a/src/Rules/Functions/MissingFunctionParameterTypehintRule.php b/src/Rules/Functions/MissingFunctionParameterTypehintRule.php index 63c6a43e79..0a02fbaca7 100644 --- a/src/Rules/Functions/MissingFunctionParameterTypehintRule.php +++ b/src/Rules/Functions/MissingFunctionParameterTypehintRule.php @@ -10,23 +10,24 @@ use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; use PHPStan\Rules\MissingTypehintCheck; +use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; use PHPStan\Type\VerbosityLevel; +use function implode; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -final class MissingFunctionParameterTypehintRule implements \PHPStan\Rules\Rule +final class MissingFunctionParameterTypehintRule implements Rule { - private \PHPStan\Rules\MissingTypehintCheck $missingTypehintCheck; - public function __construct( - MissingTypehintCheck $missingTypehintCheck + private MissingTypehintCheck $missingTypehintCheck, ) { - $this->missingTypehintCheck = $missingTypehintCheck; } public function getNodeType(): string @@ -53,9 +54,7 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param \PHPStan\Reflection\FunctionReflection $functionReflection - * @param \PHPStan\Reflection\ParameterReflection $parameterReflection - * @return \PHPStan\Rules\RuleError[] + * @return RuleError[] */ private function checkFunctionParameter(FunctionReflection $functionReflection, ParameterReflection $parameterReflection): array { @@ -64,9 +63,9 @@ private function checkFunctionParameter(FunctionReflection $functionReflection, if ($parameterType instanceof MixedType && !$parameterType->isExplicitMixed()) { return [ RuleErrorBuilder::message(sprintf( - 'Function %s() has parameter $%s with no typehint specified.', + 'Function %s() has parameter $%s with no type specified.', $functionReflection->getName(), - $parameterReflection->getName() + $parameterReflection->getName(), ))->build(), ]; } @@ -78,7 +77,7 @@ private function checkFunctionParameter(FunctionReflection $functionReflection, 'Function %s() has parameter $%s with no value type specified in iterable type %s.', $functionReflection->getName(), $parameterReflection->getName(), - $iterableTypeDescription + $iterableTypeDescription, ))->tip(MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP)->build(); } @@ -88,7 +87,7 @@ private function checkFunctionParameter(FunctionReflection $functionReflection, $functionReflection->getName(), $parameterReflection->getName(), $name, - implode(', ', $genericTypeNames) + implode(', ', $genericTypeNames), ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); } @@ -97,7 +96,7 @@ private function checkFunctionParameter(FunctionReflection $functionReflection, 'Function %s() has parameter $%s with no signature specified for %s.', $functionReflection->getName(), $parameterReflection->getName(), - $callableType->describe(VerbosityLevel::typeOnly()) + $callableType->describe(VerbosityLevel::typeOnly()), ))->build(); } diff --git a/src/Rules/Functions/MissingFunctionReturnTypehintRule.php b/src/Rules/Functions/MissingFunctionReturnTypehintRule.php index 8d75bf1578..7166ebad1a 100644 --- a/src/Rules/Functions/MissingFunctionReturnTypehintRule.php +++ b/src/Rules/Functions/MissingFunctionReturnTypehintRule.php @@ -8,23 +8,23 @@ use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; use PHPStan\Rules\MissingTypehintCheck; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; use PHPStan\Type\VerbosityLevel; +use function implode; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -final class MissingFunctionReturnTypehintRule implements \PHPStan\Rules\Rule +final class MissingFunctionReturnTypehintRule implements Rule { - private \PHPStan\Rules\MissingTypehintCheck $missingTypehintCheck; - public function __construct( - MissingTypehintCheck $missingTypehintCheck + private MissingTypehintCheck $missingTypehintCheck, ) { - $this->missingTypehintCheck = $missingTypehintCheck; } public function getNodeType(): string @@ -44,8 +44,8 @@ public function processNode(Node $node, Scope $scope): array if ($returnType instanceof MixedType && !$returnType->isExplicitMixed()) { return [ RuleErrorBuilder::message(sprintf( - 'Function %s() has no return typehint specified.', - $functionReflection->getName() + 'Function %s() has no return type specified.', + $functionReflection->getName(), ))->build(), ]; } @@ -61,7 +61,7 @@ public function processNode(Node $node, Scope $scope): array 'Function %s() return type with generic %s does not specify its types: %s', $functionReflection->getName(), $name, - implode(', ', $genericTypeNames) + implode(', ', $genericTypeNames), ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); } @@ -69,7 +69,7 @@ public function processNode(Node $node, Scope $scope): array $messages[] = RuleErrorBuilder::message(sprintf( 'Function %s() return type has no signature specified for %s.', $functionReflection->getName(), - $callableType->describe(VerbosityLevel::typeOnly()) + $callableType->describe(VerbosityLevel::typeOnly()), ))->build(); } diff --git a/src/Rules/Functions/ParamAttributesRule.php b/src/Rules/Functions/ParamAttributesRule.php index bb4aada71d..aeadb1cfb6 100644 --- a/src/Rules/Functions/ParamAttributesRule.php +++ b/src/Rules/Functions/ParamAttributesRule.php @@ -2,10 +2,12 @@ namespace PHPStan\Rules\Functions; +use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Rules\AttributesCheck; use PHPStan\Rules\Rule; +use function count; /** * @implements Rule @@ -13,11 +15,8 @@ class ParamAttributesRule implements Rule { - private AttributesCheck $attributesCheck; - - public function __construct(AttributesCheck $attributesCheck) + public function __construct(private AttributesCheck $attributesCheck) { - $this->attributesCheck = $attributesCheck; } public function getNodeType(): string @@ -34,8 +33,8 @@ public function processNode(Node $node, Scope $scope): array $propertyTargetErrors = $this->attributesCheck->check( $scope, $node->attrGroups, - \Attribute::TARGET_PROPERTY, - $targetName + Attribute::TARGET_PROPERTY, + $targetName, ); if (count($propertyTargetErrors) === 0) { @@ -46,8 +45,8 @@ public function processNode(Node $node, Scope $scope): array return $this->attributesCheck->check( $scope, $node->attrGroups, - \Attribute::TARGET_PARAMETER, - $targetName + Attribute::TARGET_PARAMETER, + $targetName, ); } diff --git a/src/Rules/Functions/PrintfParametersRule.php b/src/Rules/Functions/PrintfParametersRule.php index 83f3878b7a..b6f0552b65 100644 --- a/src/Rules/Functions/PrintfParametersRule.php +++ b/src/Rules/Functions/PrintfParametersRule.php @@ -2,24 +2,31 @@ namespace PHPStan\Rules\Functions; +use Nette\Utils\Strings; use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Php\PhpVersion; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\TypeUtils; +use function array_filter; +use function count; +use function in_array; +use function max; +use function sprintf; +use function strlen; +use function strtolower; +use const PREG_SET_ORDER; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\FuncCall> + * @implements Rule */ -class PrintfParametersRule implements \PHPStan\Rules\Rule +class PrintfParametersRule implements Rule { - private PhpVersion $phpVersion; - - public function __construct(PhpVersion $phpVersion) + public function __construct(private PhpVersion $phpVersion) { - $this->phpVersion = $phpVersion; } public function getNodeType(): string @@ -29,7 +36,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!($node->name instanceof \PhpParser\Node\Name)) { + if (!($node->name instanceof Node\Name)) { return []; } @@ -53,7 +60,7 @@ public function processNode(Node $node, Scope $scope): array $formatArgumentPosition = $functionsArgumentPositions[$name]; - $args = $node->args; + $args = $node->getArgs(); foreach ($args as $arg) { if ($arg->unpack) { return []; @@ -88,11 +95,11 @@ public function processNode(Node $node, Scope $scope): array sprintf( '%s, %s.', $placeHoldersCount === 1 ? 'Call to %s contains %d placeholder' : 'Call to %s contains %d placeholders', - $argsCount - 1 === 1 ? '%d value given' : '%d values given' + $argsCount - 1 === 1 ? '%d value given' : '%d values given', ), $name, $placeHoldersCount, - $argsCount - 1 + $argsCount - 1, ))->build(), ]; } @@ -112,15 +119,13 @@ private function getPlaceholdersCount(string $functionName, string $format): int $pattern = '~(?%*)%(?:(?\d+)\$)?[-+]?(?:[ 0]|(?:\'[^%]))?-?\d*(?:\.\d*)?' . $specifiers . '~'; - $matches = \Nette\Utils\Strings::matchAll($format, $pattern, PREG_SET_ORDER); + $matches = Strings::matchAll($format, $pattern, PREG_SET_ORDER); if (count($matches) === 0) { return 0; } - $placeholders = array_filter($matches, static function (array $match): bool { - return strlen($match['before']) % 2 === 0; - }); + $placeholders = array_filter($matches, static fn (array $match): bool => strlen($match['before']) % 2 === 0); if (count($placeholders) === 0) { return 0; diff --git a/src/Rules/Functions/RandomIntParametersRule.php b/src/Rules/Functions/RandomIntParametersRule.php index 9d7a09576c..f3ce19c174 100644 --- a/src/Rules/Functions/RandomIntParametersRule.php +++ b/src/Rules/Functions/RandomIntParametersRule.php @@ -6,25 +6,23 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\VerbosityLevel; +use function array_values; +use function count; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\FuncCall> + * @implements Rule */ -class RandomIntParametersRule implements \PHPStan\Rules\Rule +class RandomIntParametersRule implements Rule { - private ReflectionProvider $reflectionProvider; - - private bool $reportMaybes; - - public function __construct(ReflectionProvider $reflectionProvider, bool $reportMaybes) + public function __construct(private ReflectionProvider $reflectionProvider, private bool $reportMaybes) { - $this->reflectionProvider = $reflectionProvider; - $this->reportMaybes = $reportMaybes; } public function getNodeType(): string @@ -34,7 +32,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - if (!($node->name instanceof \PhpParser\Node\Name)) { + if (!($node->name instanceof Node\Name)) { return []; } @@ -42,8 +40,13 @@ public function processNode(Node $node, Scope $scope): array return []; } - $minType = $scope->getType($node->args[0]->value)->toInteger(); - $maxType = $scope->getType($node->args[1]->value)->toInteger(); + $args = array_values($node->getArgs()); + if (count($args) < 2) { + return []; + } + + $minType = $scope->getType($args[0]->value)->toInteger(); + $maxType = $scope->getType($args[1]->value)->toInteger(); if ( !$minType instanceof ConstantIntegerType && !$minType instanceof IntegerRangeType @@ -60,7 +63,7 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( $message, $minType->describe(VerbosityLevel::value()), - $maxType->describe(VerbosityLevel::value()) + $maxType->describe(VerbosityLevel::value()), ))->build(), ]; } diff --git a/src/Rules/Functions/ReturnNullsafeByRefRule.php b/src/Rules/Functions/ReturnNullsafeByRefRule.php index a232103ca1..d842cf4584 100644 --- a/src/Rules/Functions/ReturnNullsafeByRefRule.php +++ b/src/Rules/Functions/ReturnNullsafeByRefRule.php @@ -15,11 +15,8 @@ class ReturnNullsafeByRefRule implements Rule { - private NullsafeCheck $nullsafeCheck; - - public function __construct(NullsafeCheck $nullsafeCheck) + public function __construct(private NullsafeCheck $nullsafeCheck) { - $this->nullsafeCheck = $nullsafeCheck; } public function getNodeType(): string diff --git a/src/Rules/Functions/ReturnTypeRule.php b/src/Rules/Functions/ReturnTypeRule.php index a554fcba21..ab9897713b 100644 --- a/src/Rules/Functions/ReturnTypeRule.php +++ b/src/Rules/Functions/ReturnTypeRule.php @@ -5,30 +5,23 @@ use PhpParser\Node; use PhpParser\Node\Stmt\Return_; use PHPStan\Analyser\Scope; -use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; -use PHPStan\BetterReflection\Reflector\FunctionReflector; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Rules\FunctionReturnTypeCheck; +use PHPStan\Rules\Rule; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Return_> + * @implements Rule */ -class ReturnTypeRule implements \PHPStan\Rules\Rule +class ReturnTypeRule implements Rule { - private \PHPStan\Rules\FunctionReturnTypeCheck $returnTypeCheck; - - private FunctionReflector $functionReflector; - public function __construct( - FunctionReturnTypeCheck $returnTypeCheck, - FunctionReflector $functionReflector + private FunctionReturnTypeCheck $returnTypeCheck, ) { - $this->returnTypeCheck = $returnTypeCheck; - $this->functionReflector = $functionReflector; } public function getNodeType(): string @@ -54,17 +47,6 @@ public function processNode(Node $node, Scope $scope): array return []; } - $reflection = null; - if (function_exists($function->getName())) { - $reflection = new \ReflectionFunction($function->getName()); - } else { - try { - $reflection = $this->functionReflector->reflect($function->getName()); - } catch (IdentifierNotFound $e) { - // pass - } - } - return $this->returnTypeCheck->checkReturnType( $scope, ParametersAcceptorSelector::selectSingle($function->getVariants())->getReturnType(), @@ -72,21 +54,21 @@ public function processNode(Node $node, Scope $scope): array $node, sprintf( 'Function %s() should return %%s but empty return statement found.', - $function->getName() + $function->getName(), ), sprintf( 'Function %s() with return type void returns %%s but should not return anything.', - $function->getName() + $function->getName(), ), sprintf( 'Function %s() should return %%s but returns %%s.', - $function->getName() + $function->getName(), ), sprintf( 'Function %s() should never return but return statement found.', - $function->getName() + $function->getName(), ), - $reflection !== null && $reflection->isGenerator() + $function->isGenerator(), ); } diff --git a/src/Rules/Functions/UnusedClosureUsesRule.php b/src/Rules/Functions/UnusedClosureUsesRule.php index d26dac08a1..79b7268c9b 100644 --- a/src/Rules/Functions/UnusedClosureUsesRule.php +++ b/src/Rules/Functions/UnusedClosureUsesRule.php @@ -4,19 +4,21 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\UnusedFunctionParametersCheck; +use PHPStan\ShouldNotHappenException; +use function array_map; +use function count; +use function is_string; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Closure> + * @implements Rule */ -class UnusedClosureUsesRule implements \PHPStan\Rules\Rule +class UnusedClosureUsesRule implements Rule { - private \PHPStan\Rules\UnusedFunctionParametersCheck $check; - - public function __construct(UnusedFunctionParametersCheck $check) + public function __construct(private UnusedFunctionParametersCheck $check) { - $this->check = $check; } public function getNodeType(): string @@ -34,7 +36,7 @@ public function processNode(Node $node, Scope $scope): array $scope, array_map(static function (Node\Expr\ClosureUse $use): string { if (!is_string($use->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $use->var->name; }, $node->uses), @@ -46,7 +48,7 @@ public function processNode(Node $node, Scope $scope): array 'statementOrder' => $node->getAttribute('statementOrder'), 'depth' => $node->getAttribute('expressionDepth'), 'order' => $node->getAttribute('expressionOrder'), - ] + ], ); } diff --git a/src/Rules/Generators/YieldFromTypeRule.php b/src/Rules/Generators/YieldFromTypeRule.php index 8e6980eae2..8926b3f270 100644 --- a/src/Rules/Generators/YieldFromTypeRule.php +++ b/src/Rules/Generators/YieldFromTypeRule.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Generators; +use Generator; use PhpParser\Node; use PhpParser\Node\Expr\YieldFrom; use PHPStan\Analyser\Scope; @@ -14,24 +15,19 @@ use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VerbosityLevel; use PHPStan\Type\VoidType; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\YieldFrom> + * @implements Rule */ class YieldFromTypeRule implements Rule { - private RuleLevelHelper $ruleLevelHelper; - - private bool $reportMaybes; - public function __construct( - RuleLevelHelper $ruleLevelHelper, - bool $reportMaybes + private RuleLevelHelper $ruleLevelHelper, + private bool $reportMaybes, ) { - $this->ruleLevelHelper = $ruleLevelHelper; - $this->reportMaybes = $reportMaybes; } public function getNodeType(): string @@ -48,7 +44,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message(sprintf( $messagePattern, - $exprType->describe(VerbosityLevel::typeOnly()) + $exprType->describe(VerbosityLevel::typeOnly()), ))->line($node->expr->getLine())->build(), ]; } elseif ( @@ -59,7 +55,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message(sprintf( $messagePattern, - $exprType->describe(VerbosityLevel::typeOnly()) + $exprType->describe(VerbosityLevel::typeOnly()), ))->line($node->expr->getLine())->build(), ]; } @@ -84,7 +80,7 @@ public function processNode(Node $node, Scope $scope): array $messages[] = RuleErrorBuilder::message(sprintf( 'Generator expects key type %s, %s given.', $returnType->getIterableKeyType()->describe($verbosityLevel), - $exprType->getIterableKeyType()->describe($verbosityLevel) + $exprType->getIterableKeyType()->describe($verbosityLevel), ))->line($node->expr->getLine())->build(); } if (!$this->ruleLevelHelper->accepts($returnType->getIterableValueType(), $exprType->getIterableValueType(), $scope->isDeclareStrictTypes())) { @@ -92,7 +88,7 @@ public function processNode(Node $node, Scope $scope): array $messages[] = RuleErrorBuilder::message(sprintf( 'Generator expects value type %s, %s given.', $returnType->getIterableValueType()->describe($verbosityLevel), - $exprType->getIterableValueType()->describe($verbosityLevel) + $exprType->getIterableValueType()->describe($verbosityLevel), ))->line($node->expr->getLine())->build(); } @@ -110,8 +106,8 @@ public function processNode(Node $node, Scope $scope): array return $messages; } - $exprSendType = GenericTypeVariableResolver::getType($exprType, \Generator::class, 'TSend'); - $thisSendType = GenericTypeVariableResolver::getType($currentReturnType, \Generator::class, 'TSend'); + $exprSendType = GenericTypeVariableResolver::getType($exprType, Generator::class, 'TSend'); + $thisSendType = GenericTypeVariableResolver::getType($currentReturnType, Generator::class, 'TSend'); if ($exprSendType === null || $thisSendType === null) { return $messages; } @@ -121,13 +117,13 @@ public function processNode(Node $node, Scope $scope): array $messages[] = RuleErrorBuilder::message(sprintf( 'Generator expects delegated TSend type %s, %s given.', $exprSendType->describe(VerbosityLevel::typeOnly()), - $thisSendType->describe(VerbosityLevel::typeOnly()) + $thisSendType->describe(VerbosityLevel::typeOnly()), ))->build(); } elseif ($this->reportMaybes && !$isSuperType->yes()) { $messages[] = RuleErrorBuilder::message(sprintf( 'Generator expects delegated TSend type %s, %s given.', $exprSendType->describe(VerbosityLevel::typeOnly()), - $thisSendType->describe(VerbosityLevel::typeOnly()) + $thisSendType->describe(VerbosityLevel::typeOnly()), ))->build(); } diff --git a/src/Rules/Generators/YieldInGeneratorRule.php b/src/Rules/Generators/YieldInGeneratorRule.php index 559dc7f090..230853ca38 100644 --- a/src/Rules/Generators/YieldInGeneratorRule.php +++ b/src/Rules/Generators/YieldInGeneratorRule.php @@ -10,18 +10,16 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr> + * @implements Rule */ class YieldInGeneratorRule implements Rule { - private bool $reportMaybes; - - public function __construct(bool $reportMaybes) + public function __construct(private bool $reportMaybes) { - $this->reportMaybes = $reportMaybes; } public function getNodeType(): string @@ -53,7 +51,7 @@ public function processNode(Node $node, Scope $scope): array $isSuperType = TrinaryLogic::createNo(); } else { $isSuperType = $returnType->isIterable()->and(TrinaryLogic::createFromBoolean( - !$returnType->isArray()->yes() + !$returnType->isArray()->yes(), )); } if ($isSuperType->yes()) { @@ -67,7 +65,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message(sprintf( 'Yield can be used only with these return types: %s.', - 'Generator, Iterator, Traversable, iterable' + 'Generator, Iterator, Traversable, iterable', ))->build(), ]; } diff --git a/src/Rules/Generators/YieldTypeRule.php b/src/Rules/Generators/YieldTypeRule.php index f0b04ba578..c5678f8a66 100644 --- a/src/Rules/Generators/YieldTypeRule.php +++ b/src/Rules/Generators/YieldTypeRule.php @@ -13,20 +13,18 @@ use PHPStan\Type\NullType; use PHPStan\Type\VerbosityLevel; use PHPStan\Type\VoidType; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Yield_> + * @implements Rule */ class YieldTypeRule implements Rule { - private RuleLevelHelper $ruleLevelHelper; - public function __construct( - RuleLevelHelper $ruleLevelHelper + private RuleLevelHelper $ruleLevelHelper, ) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string @@ -68,7 +66,7 @@ public function processNode(Node $node, Scope $scope): array $messages[] = RuleErrorBuilder::message(sprintf( 'Generator expects key type %s, %s given.', $returnType->getIterableKeyType()->describe($verbosityLevel), - $keyType->describe($verbosityLevel) + $keyType->describe($verbosityLevel), ))->build(); } if (!$this->ruleLevelHelper->accepts($returnType->getIterableValueType(), $valueType, $scope->isDeclareStrictTypes())) { @@ -76,7 +74,7 @@ public function processNode(Node $node, Scope $scope): array $messages[] = RuleErrorBuilder::message(sprintf( 'Generator expects value type %s, %s given.', $returnType->getIterableValueType()->describe($verbosityLevel), - $valueType->describe($verbosityLevel) + $valueType->describe($verbosityLevel), ))->build(); } if ($scope->getType($node) instanceof VoidType && !$scope->isInFirstLevelStatement()) { diff --git a/src/Rules/Generics/ClassAncestorsRule.php b/src/Rules/Generics/ClassAncestorsRule.php index d6087dbfe7..4c77d8899a 100644 --- a/src/Rules/Generics/ClassAncestorsRule.php +++ b/src/Rules/Generics/ClassAncestorsRule.php @@ -4,94 +4,84 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; +use PHPStan\Node\InClassNode; use PHPStan\PhpDoc\Tag\ExtendsTag; use PHPStan\PhpDoc\Tag\ImplementsTag; use PHPStan\Rules\Rule; -use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Type; +use function array_map; +use function array_merge; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Class_> + * @implements Rule */ class ClassAncestorsRule implements Rule { - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - private \PHPStan\Rules\Generics\GenericAncestorsCheck $genericAncestorsCheck; - public function __construct( - FileTypeMapper $fileTypeMapper, - GenericAncestorsCheck $genericAncestorsCheck + private GenericAncestorsCheck $genericAncestorsCheck, + private CrossCheckInterfacesHelper $crossCheckInterfacesHelper, ) { - $this->fileTypeMapper = $fileTypeMapper; - $this->genericAncestorsCheck = $genericAncestorsCheck; } public function getNodeType(): string { - return Node\Stmt\Class_::class; + return InClassNode::class; } public function processNode(Node $node, Scope $scope): array { - if (!isset($node->namespacedName)) { - // anonymous class + $originalNode = $node->getOriginalNode(); + if (!$originalNode instanceof Node\Stmt\Class_) { return []; } - - $className = (string) $node->namespacedName; - - $extendsTags = []; - $implementsTags = []; - $docComment = $node->getDocComment(); - if ($docComment !== null) { - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $scope->getFile(), - $className, - null, - null, - $docComment->getText() - ); - $extendsTags = $resolvedPhpDoc->getExtendsTags(); - $implementsTags = $resolvedPhpDoc->getImplementsTags(); + if (!$scope->isInClass()) { + return []; } + $classReflection = $scope->getClassReflection(); + if ($classReflection->isAnonymous()) { + return []; + } + $className = $classReflection->getName(); + $escapedClassName = SprintfHelper::escapeFormatString($className); $extendsErrors = $this->genericAncestorsCheck->check( - $node->extends !== null ? [$node->extends] : [], - array_map(static function (ExtendsTag $tag): Type { - return $tag->getType(); - }, $extendsTags), - sprintf('Class %s @extends tag contains incompatible type %%s.', $className), - sprintf('Class %s has @extends tag, but does not extend any class.', $className), - sprintf('The @extends tag of class %s describes %%s but the class extends %%s.', $className), - 'PHPDoc tag @extends contains generic type %s but class %s is not generic.', - 'Generic type %s in PHPDoc tag @extends does not specify all template types of class %s: %s', - 'Generic type %s in PHPDoc tag @extends specifies %d template types, but class %s supports only %d: %s', - 'Type %s in generic type %s in PHPDoc tag @extends is not subtype of template type %s of class %s.', + $originalNode->extends !== null ? [$originalNode->extends] : [], + array_map(static fn (ExtendsTag $tag): Type => $tag->getType(), $classReflection->getExtendsTags()), + sprintf('Class %s @extends tag contains incompatible type %%s.', $escapedClassName), + sprintf('Class %s has @extends tag, but does not extend any class.', $escapedClassName), + sprintf('The @extends tag of class %s describes %%s but the class extends %%s.', $escapedClassName), + 'PHPDoc tag @extends contains generic type %s but %s %s is not generic.', + '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.', 'PHPDoc tag @extends has invalid type %s.', - sprintf('Class %s extends generic class %%s but does not specify its types: %%s', $className), - sprintf('in extended type %%s of class %s', $className) + sprintf('Class %s extends generic class %%s but does not specify its types: %%s', $escapedClassName), + sprintf('in extended type %%s of class %s', $escapedClassName), ); $implementsErrors = $this->genericAncestorsCheck->check( - $node->implements, - array_map(static function (ImplementsTag $tag): Type { - return $tag->getType(); - }, $implementsTags), - sprintf('Class %s @implements tag contains incompatible type %%s.', $className), - sprintf('Class %s has @implements tag, but does not implement any interface.', $className), - sprintf('The @implements tag of class %s describes %%s but the class implements: %%s', $className), - 'PHPDoc tag @implements contains generic type %s but interface %s is not generic.', - 'Generic type %s in PHPDoc tag @implements does not specify all template types of interface %s: %s', - 'Generic type %s in PHPDoc tag @implements specifies %d template types, but interface %s supports only %d: %s', - 'Type %s in generic type %s in PHPDoc tag @implements is not subtype of template type %s of interface %s.', + $originalNode->implements, + array_map(static fn (ImplementsTag $tag): Type => $tag->getType(), $classReflection->getImplementsTags()), + sprintf('Class %s @implements tag contains incompatible type %%s.', $escapedClassName), + sprintf('Class %s has @implements tag, but does not implement any interface.', $escapedClassName), + sprintf('The @implements tag of class %s describes %%s but the class implements: %%s', $escapedClassName), + 'PHPDoc tag @implements contains generic type %s but %s %s is not generic.', + '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.', 'PHPDoc tag @implements has invalid type %s.', - sprintf('Class %s implements generic interface %%s but does not specify its types: %%s', $className), - sprintf('in implemented type %%s of class %s', $className) + sprintf('Class %s implements generic interface %%s but does not specify its types: %%s', $escapedClassName), + sprintf('in implemented type %%s of class %s', $escapedClassName), ); + foreach ($this->crossCheckInterfacesHelper->check($classReflection) as $error) { + $implementsErrors[] = $error; + } + return array_merge($extendsErrors, $implementsErrors); } diff --git a/src/Rules/Generics/ClassTemplateTypeRule.php b/src/Rules/Generics/ClassTemplateTypeRule.php index ec726ce894..5e24afd305 100644 --- a/src/Rules/Generics/ClassTemplateTypeRule.php +++ b/src/Rules/Generics/ClassTemplateTypeRule.php @@ -4,23 +4,22 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InClassNode; use PHPStan\Rules\Rule; use PHPStan\Type\Generic\TemplateTypeScope; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ class ClassTemplateTypeRule implements Rule { - private \PHPStan\Rules\Generics\TemplateTypeCheck $templateTypeCheck; - public function __construct( - TemplateTypeCheck $templateTypeCheck + private TemplateTypeCheck $templateTypeCheck, ) { - $this->templateTypeCheck = $templateTypeCheck; } public function getNodeType(): string @@ -34,11 +33,14 @@ public function processNode(Node $node, Scope $scope): array return []; } $classReflection = $scope->getClassReflection(); + if (!$classReflection->isClass()) { + return []; + } $className = $classReflection->getName(); if ($classReflection->isAnonymous()) { $displayName = 'anonymous class'; } else { - $displayName = 'class ' . $classReflection->getDisplayName(); + $displayName = 'class ' . SprintfHelper::escapeFormatString($classReflection->getDisplayName()); } return $this->templateTypeCheck->check( @@ -48,7 +50,7 @@ public function processNode(Node $node, Scope $scope): array sprintf('PHPDoc tag @template for %s cannot have existing class %%s as its name.', $displayName), sprintf('PHPDoc tag @template for %s cannot have existing type alias %%s as its name.', $displayName), sprintf('PHPDoc tag @template %%s for %s has invalid bound type %%s.', $displayName), - sprintf('PHPDoc tag @template %%s for %s with bound type %%s is not supported.', $displayName) + sprintf('PHPDoc tag @template %%s for %s with bound type %%s is not supported.', $displayName), ); } diff --git a/src/Rules/Generics/CrossCheckInterfacesHelper.php b/src/Rules/Generics/CrossCheckInterfacesHelper.php new file mode 100644 index 0000000000..f272063e24 --- /dev/null +++ b/src/Rules/Generics/CrossCheckInterfacesHelper.php @@ -0,0 +1,94 @@ +getInterfaces() as $interface) { + if (!$interface->isGeneric()) { + continue; + } + + if (array_key_exists($interface->getName(), $interfaceTemplateTypeMaps)) { + $otherMap = $interfaceTemplateTypeMaps[$interface->getName()]; + foreach ($interface->getActiveTemplateTypeMap()->getTypes() as $name => $type) { + $otherType = $otherMap->getType($name); + if ($otherType === null) { + continue; + } + + if ($type->equals($otherType)) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + '%s specifies template type %s of interface %s as %s but it\'s already specified as %s.', + $classReflection->isInterface() ? sprintf('Interface %s', $classReflection->getName()) : sprintf('Class %s', $classReflection->getName()), + $name, + $interface->getName(), + $type->describe(VerbosityLevel::value()), + $otherType->describe(VerbosityLevel::value()), + ))->build(); + } + continue; + } + + $interfaceTemplateTypeMaps[$interface->getName()] = $interface->getActiveTemplateTypeMap(); + } + + $parent = $classReflection->getParentClass(); + $checkParents = true; + if ($first && $parent !== null) { + $extendsTags = $classReflection->getExtendsTags(); + if (!array_key_exists($parent->getName(), $extendsTags)) { + $checkParents = false; + } + } + + if ($checkParents) { + while ($parent !== null) { + $check($parent, false); + $parent = $parent->getParentClass(); + } + } + + $interfaceTags = []; + if ($first) { + if ($classReflection->isInterface()) { + $interfaceTags = $classReflection->getExtendsTags(); + } else { + $interfaceTags = $classReflection->getImplementsTags(); + } + } + foreach ($classReflection->getInterfaces() as $interface) { + if ($first) { + if (!array_key_exists($interface->getName(), $interfaceTags)) { + continue; + } + } + $check($interface, false); + } + }; + + $check($classReflection, true); + + return $errors; + } + +} diff --git a/src/Rules/Generics/EnumAncestorsRule.php b/src/Rules/Generics/EnumAncestorsRule.php new file mode 100644 index 0000000000..63219efb22 --- /dev/null +++ b/src/Rules/Generics/EnumAncestorsRule.php @@ -0,0 +1,86 @@ + + */ +class EnumAncestorsRule implements Rule +{ + + public function __construct( + private GenericAncestorsCheck $genericAncestorsCheck, + private CrossCheckInterfacesHelper $crossCheckInterfacesHelper, + ) + { + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $originalNode = $node->getOriginalNode(); + if (!$originalNode instanceof Node\Stmt\Enum_) { + return []; + } + if (!$scope->isInClass()) { + return []; + } + $classReflection = $scope->getClassReflection(); + + $enumName = $classReflection->getName(); + $escapedEnumName = SprintfHelper::escapeFormatString($enumName); + + $extendsErrors = $this->genericAncestorsCheck->check( + [], + array_map(static fn (ExtendsTag $tag): Type => $tag->getType(), $classReflection->getExtendsTags()), + sprintf('Enum %s @extends tag contains incompatible type %%s.', $escapedEnumName), + sprintf('Enum %s has @extends tag, but cannot extend anything.', $escapedEnumName), + '', + '', + '', + '', + '', + '', + '', + '', + ); + + $implementsErrors = $this->genericAncestorsCheck->check( + $originalNode->implements, + array_map(static fn (ImplementsTag $tag): Type => $tag->getType(), $classReflection->getImplementsTags()), + sprintf('Enum %s @implements tag contains incompatible type %%s.', $escapedEnumName), + sprintf('Enum %s has @implements tag, but does not implement any interface.', $escapedEnumName), + sprintf('The @implements tag of eunm %s describes %%s but the enum implements: %%s', $escapedEnumName), + 'PHPDoc tag @implements contains generic type %s but %s %s is not generic.', + '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.', + '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), + ); + + foreach ($this->crossCheckInterfacesHelper->check($classReflection) as $error) { + $implementsErrors[] = $error; + } + + return array_merge($extendsErrors, $implementsErrors); + } + +} diff --git a/src/Rules/Generics/EnumTemplateTypeRule.php b/src/Rules/Generics/EnumTemplateTypeRule.php new file mode 100644 index 0000000000..a52ac23255 --- /dev/null +++ b/src/Rules/Generics/EnumTemplateTypeRule.php @@ -0,0 +1,46 @@ + + */ +class EnumTemplateTypeRule implements Rule +{ + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$scope->isInClass()) { + return []; + } + $classReflection = $scope->getClassReflection(); + if (!$classReflection->isEnum()) { + return []; + } + + $templateTagsCount = count($classReflection->getTemplateTags()); + if ($templateTagsCount === 0) { + return []; + } + + $className = $classReflection->getDisplayName(); + + return [ + RuleErrorBuilder::message(sprintf('Enum %s has PHPDoc @template tag%s but enums cannot be generic.', $className, $templateTagsCount === 1 ? '' : 's'))->build(), + ]; + } + +} diff --git a/src/Rules/Generics/FunctionSignatureVarianceRule.php b/src/Rules/Generics/FunctionSignatureVarianceRule.php index a6d4699100..9b75ab2f57 100644 --- a/src/Rules/Generics/FunctionSignatureVarianceRule.php +++ b/src/Rules/Generics/FunctionSignatureVarianceRule.php @@ -4,21 +4,20 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InFunctionNode; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ class FunctionSignatureVarianceRule implements Rule { - private \PHPStan\Rules\Generics\VarianceCheck $varianceCheck; - - public function __construct(VarianceCheck $varianceCheck) + public function __construct(private VarianceCheck $varianceCheck) { - $this->varianceCheck = $varianceCheck; } public function getNodeType(): string @@ -37,10 +36,10 @@ public function processNode(Node $node, Scope $scope): array return $this->varianceCheck->checkParametersAcceptor( ParametersAcceptorSelector::selectSingle($functionReflection->getVariants()), - sprintf('in parameter %%s of function %s()', $functionName), + sprintf('in parameter %%s of function %s()', SprintfHelper::escapeFormatString($functionName)), sprintf('in return type of function %s()', $functionName), sprintf('in function %s()', $functionName), - false + false, ); } diff --git a/src/Rules/Generics/FunctionTemplateTypeRule.php b/src/Rules/Generics/FunctionTemplateTypeRule.php index 2a6d2bc921..5353d0a9cf 100644 --- a/src/Rules/Generics/FunctionTemplateTypeRule.php +++ b/src/Rules/Generics/FunctionTemplateTypeRule.php @@ -4,27 +4,24 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Rules\Rule; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\TemplateTypeScope; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Function_> + * @implements Rule */ class FunctionTemplateTypeRule implements Rule { - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - private \PHPStan\Rules\Generics\TemplateTypeCheck $templateTypeCheck; - public function __construct( - FileTypeMapper $fileTypeMapper, - TemplateTypeCheck $templateTypeCheck + private FileTypeMapper $fileTypeMapper, + private TemplateTypeCheck $templateTypeCheck, ) { - $this->fileTypeMapper = $fileTypeMapper; - $this->templateTypeCheck = $templateTypeCheck; } public function getNodeType(): string @@ -40,7 +37,7 @@ public function processNode(Node $node, Scope $scope): array } if (!isset($node->namespacedName)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $functionName = (string) $node->namespacedName; @@ -49,17 +46,19 @@ public function processNode(Node $node, Scope $scope): array null, null, $functionName, - $docComment->getText() + $docComment->getText(), ); + $escapedFunctionName = SprintfHelper::escapeFormatString($functionName); + return $this->templateTypeCheck->check( $node, TemplateTypeScope::createWithFunction($functionName), $resolvedPhpDoc->getTemplateTags(), - sprintf('PHPDoc tag @template for function %s() cannot have existing class %%s as its name.', $functionName), - sprintf('PHPDoc tag @template for function %s() cannot have existing type alias %%s as its name.', $functionName), - sprintf('PHPDoc tag @template %%s for function %s() has invalid bound type %%s.', $functionName), - sprintf('PHPDoc tag @template %%s for function %s() with bound type %%s is not supported.', $functionName) + sprintf('PHPDoc tag @template for function %s() cannot have existing class %%s as its name.', $escapedFunctionName), + sprintf('PHPDoc tag @template for function %s() cannot have existing type alias %%s as its name.', $escapedFunctionName), + sprintf('PHPDoc tag @template %%s for function %s() has invalid bound type %%s.', $escapedFunctionName), + sprintf('PHPDoc tag @template %%s for function %s() with bound type %%s is not supported.', $escapedFunctionName), ); } diff --git a/src/Rules/Generics/GenericAncestorsCheck.php b/src/Rules/Generics/GenericAncestorsCheck.php index c97a8043cc..4210bf7e29 100644 --- a/src/Rules/Generics/GenericAncestorsCheck.php +++ b/src/Rules/Generics/GenericAncestorsCheck.php @@ -2,50 +2,45 @@ namespace PHPStan\Rules\Generics; +use PhpParser\Node; use PhpParser\Node\Name; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\MissingTypehintCheck; +use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function array_fill_keys; +use function array_keys; +use function array_map; +use function array_merge; +use function count; +use function implode; +use function in_array; +use function sprintf; class GenericAncestorsCheck { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\Generics\GenericObjectTypeCheck $genericObjectTypeCheck; - - private \PHPStan\Rules\Generics\VarianceCheck $varianceCheck; - - private bool $checkGenericClassInNonGenericObjectType; - - /** @var string[] */ - private array $skipCheckGenericClasses; - /** * @param string[] $skipCheckGenericClasses */ public function __construct( - ReflectionProvider $reflectionProvider, - GenericObjectTypeCheck $genericObjectTypeCheck, - VarianceCheck $varianceCheck, - bool $checkGenericClassInNonGenericObjectType, - array $skipCheckGenericClasses = [] + private ReflectionProvider $reflectionProvider, + private GenericObjectTypeCheck $genericObjectTypeCheck, + private VarianceCheck $varianceCheck, + private bool $checkGenericClassInNonGenericObjectType, + private array $skipCheckGenericClasses, ) { - $this->reflectionProvider = $reflectionProvider; - $this->genericObjectTypeCheck = $genericObjectTypeCheck; - $this->varianceCheck = $varianceCheck; - $this->checkGenericClassInNonGenericObjectType = $checkGenericClassInNonGenericObjectType; - $this->skipCheckGenericClasses = $skipCheckGenericClasses; } /** - * @param array<\PhpParser\Node\Name> $nameNodes - * @param array<\PHPStan\Type\Type> $ancestorTypes - * @return \PHPStan\Rules\RuleError[] + * @param array $nameNodes + * @param array $ancestorTypes + * @return RuleError[] */ public function check( array $nameNodes, @@ -59,12 +54,10 @@ public function check( string $typeIsNotSubtypeMessage, string $invalidTypeMessage, string $genericClassInNonGenericObjectType, - string $invalidVarianceMessage + string $invalidVarianceMessage, ): array { - $names = array_fill_keys(array_map(static function (Name $nameNode): string { - return $nameNode->toString(); - }, $nameNodes), true); + $names = array_fill_keys(array_map(static fn (Name $nameNode): string => $nameNode->toString(), $nameNodes), true); $unusedNames = $names; @@ -93,7 +86,7 @@ public function check( $classNotGenericMessage, $notEnoughTypesMessage, $extraTypesMessage, - $typeIsNotSubtypeMessage + $typeIsNotSubtypeMessage, ); $messages = array_merge($messages, $genericObjectTypeCheckMessages); @@ -108,7 +101,7 @@ public function check( $variance = TemplateTypeVariance::createInvariant(); $messageContext = sprintf( $invalidVarianceMessage, - $ancestorType->describe(VerbosityLevel::typeOnly()) + $ancestorType->describe(VerbosityLevel::typeOnly()), ); foreach ($this->varianceCheck->check($variance, $ancestorType, $messageContext) as $message) { $messages[] = $message; @@ -132,7 +125,7 @@ public function check( $messages[] = RuleErrorBuilder::message(sprintf( $genericClassInNonGenericObjectType, $unusedName, - implode(', ', array_keys($unusedNameClassReflection->getTemplateTypeMap()->getTypes())) + implode(', ', array_keys($unusedNameClassReflection->getTemplateTypeMap()->getTypes())), ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); } } diff --git a/src/Rules/Generics/GenericObjectTypeCheck.php b/src/Rules/Generics/GenericObjectTypeCheck.php index 16eea0ed66..db2eaf27d6 100644 --- a/src/Rules/Generics/GenericObjectTypeCheck.php +++ b/src/Rules/Generics/GenericObjectTypeCheck.php @@ -2,31 +2,33 @@ namespace PHPStan\Rules\Generics; +use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; use PHPStan\Type\VerbosityLevel; +use function array_keys; +use function array_values; +use function count; +use function implode; +use function sprintf; class GenericObjectTypeCheck { /** - * @param \PHPStan\Type\Type $phpDocType - * @param string $classNotGenericMessage - * @param string $notEnoughTypesMessage - * @param string $extraTypesMessage - * @param string $typeIsNotSubtypeMessage - * @return \PHPStan\Rules\RuleError[] + * @return RuleError[] */ public function check( Type $phpDocType, string $classNotGenericMessage, string $notEnoughTypesMessage, string $extraTypesMessage, - string $typeIsNotSubtypeMessage + string $typeIsNotSubtypeMessage, ): array { $genericTypes = $this->getGenericTypes($phpDocType); @@ -36,8 +38,17 @@ public function check( if ($classReflection === null) { continue; } + + $classLikeDescription = 'class'; + if ($classReflection->isInterface()) { + $classLikeDescription = 'interface'; + } elseif ($classReflection->isTrait()) { + $classLikeDescription = 'trait'; + } elseif ($classReflection->isEnum()) { + $classLikeDescription = 'enum'; + } if (!$classReflection->isGeneric()) { - $messages[] = RuleErrorBuilder::message(sprintf($classNotGenericMessage, $genericType->describe(VerbosityLevel::typeOnly()), $classReflection->getDisplayName()))->build(); + $messages[] = RuleErrorBuilder::message(sprintf($classNotGenericMessage, $genericType->describe(VerbosityLevel::typeOnly()), $classLikeDescription, $classReflection->getDisplayName()))->build(); continue; } @@ -50,17 +61,19 @@ public function check( $messages[] = RuleErrorBuilder::message(sprintf( $notEnoughTypesMessage, $genericType->describe(VerbosityLevel::typeOnly()), + $classLikeDescription, $classReflection->getDisplayName(false), - implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes())) + implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes())), ))->build(); } elseif ($templateTypesCount < $genericTypeTypesCount) { $messages[] = RuleErrorBuilder::message(sprintf( $extraTypesMessage, $genericType->describe(VerbosityLevel::typeOnly()), $genericTypeTypesCount, + $classLikeDescription, $classReflection->getDisplayName(false), $templateTypesCount, - implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes())) + implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes())), ))->build(); } @@ -93,7 +106,8 @@ public function check( $genericTypeType->describe(VerbosityLevel::typeOnly()), $genericType->describe(VerbosityLevel::typeOnly()), $templateType->describe(VerbosityLevel::typeOnly()), - $classReflection->getDisplayName(false) + $classLikeDescription, + $classReflection->getDisplayName(false), ))->build(); } } @@ -102,8 +116,7 @@ public function check( } /** - * @param \PHPStan\Type\Type $phpDocType - * @return \PHPStan\Type\Generic\GenericObjectType[] + * @return GenericObjectType[] */ private function getGenericTypes(Type $phpDocType): array { @@ -112,7 +125,7 @@ private function getGenericTypes(Type $phpDocType): array if ($type instanceof GenericObjectType) { $resolvedType = TemplateTypeHelper::resolveToBounds($type); if (!$resolvedType instanceof GenericObjectType) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $genericObjectTypes[] = $resolvedType; $traverse($type); diff --git a/src/Rules/Generics/InterfaceAncestorsRule.php b/src/Rules/Generics/InterfaceAncestorsRule.php index 5a7f294113..bb207f5ae6 100644 --- a/src/Rules/Generics/InterfaceAncestorsRule.php +++ b/src/Rules/Generics/InterfaceAncestorsRule.php @@ -4,82 +4,69 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; +use PHPStan\Node\InClassNode; use PHPStan\PhpDoc\Tag\ExtendsTag; use PHPStan\PhpDoc\Tag\ImplementsTag; use PHPStan\Rules\Rule; -use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Type; +use function array_map; +use function array_merge; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Interface_> + * @implements Rule */ class InterfaceAncestorsRule implements Rule { - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - private \PHPStan\Rules\Generics\GenericAncestorsCheck $genericAncestorsCheck; - public function __construct( - FileTypeMapper $fileTypeMapper, - GenericAncestorsCheck $genericAncestorsCheck + private GenericAncestorsCheck $genericAncestorsCheck, + private CrossCheckInterfacesHelper $crossCheckInterfacesHelper, ) { - $this->fileTypeMapper = $fileTypeMapper; - $this->genericAncestorsCheck = $genericAncestorsCheck; } public function getNodeType(): string { - return Node\Stmt\Interface_::class; + return InClassNode::class; } public function processNode(Node $node, Scope $scope): array { - if (!isset($node->namespacedName)) { - throw new \PHPStan\ShouldNotHappenException(); + $originalNode = $node->getOriginalNode(); + if (!$originalNode instanceof Node\Stmt\Interface_) { + return []; } - - $interfaceName = (string) $node->namespacedName; - $extendsTags = []; - $implementsTags = []; - $docComment = $node->getDocComment(); - if ($docComment !== null) { - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $scope->getFile(), - $interfaceName, - null, - null, - $docComment->getText() - ); - $extendsTags = $resolvedPhpDoc->getExtendsTags(); - $implementsTags = $resolvedPhpDoc->getImplementsTags(); + if (!$scope->isInClass()) { + return []; } + $classReflection = $scope->getClassReflection(); + + $interfaceName = $classReflection->getName(); + $escapedInterfaceName = SprintfHelper::escapeFormatString($interfaceName); $extendsErrors = $this->genericAncestorsCheck->check( - $node->extends, - array_map(static function (ExtendsTag $tag): Type { - return $tag->getType(); - }, $extendsTags), - sprintf('Interface %s @extends tag contains incompatible type %%s.', $interfaceName), - sprintf('Interface %s has @extends tag, but does not extend any interface.', $interfaceName), - sprintf('The @extends tag of interface %s describes %%s but the interface extends: %%s', $interfaceName), - 'PHPDoc tag @extends contains generic type %s but interface %s is not generic.', - 'Generic type %s in PHPDoc tag @extends does not specify all template types of interface %s: %s', - 'Generic type %s in PHPDoc tag @extends specifies %d template types, but interface %s supports only %d: %s', - 'Type %s in generic type %s in PHPDoc tag @extends is not subtype of template type %s of interface %s.', + $originalNode->extends, + array_map(static fn (ExtendsTag $tag): Type => $tag->getType(), $classReflection->getExtendsTags()), + sprintf('Interface %s @extends tag contains incompatible type %%s.', $escapedInterfaceName), + sprintf('Interface %s has @extends tag, but does not extend any interface.', $escapedInterfaceName), + sprintf('The @extends tag of interface %s describes %%s but the interface extends: %%s', $escapedInterfaceName), + 'PHPDoc tag @extends contains generic type %s but %s %s is not generic.', + '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.', 'PHPDoc tag @extends has invalid type %s.', - sprintf('Interface %s extends generic interface %%s but does not specify its types: %%s', $interfaceName), - sprintf('in extended type %%s of interface %s', $interfaceName) + sprintf('Interface %s extends generic interface %%s but does not specify its types: %%s', $escapedInterfaceName), + sprintf('in extended type %%s of interface %s', $escapedInterfaceName), ); $implementsErrors = $this->genericAncestorsCheck->check( [], - array_map(static function (ImplementsTag $tag): Type { - return $tag->getType(); - }, $implementsTags), - sprintf('Interface %s @implements tag contains incompatible type %%s.', $interfaceName), - sprintf('Interface %s has @implements tag, but can not implement any interface, must extend from it.', $interfaceName), + array_map(static fn (ImplementsTag $tag): Type => $tag->getType(), $classReflection->getImplementsTags()), + sprintf('Interface %s @implements tag contains incompatible type %%s.', $escapedInterfaceName), + sprintf('Interface %s has @implements tag, but can not implement any interface, must extend from it.', $escapedInterfaceName), + '', '', '', '', @@ -87,9 +74,12 @@ public function processNode(Node $node, Scope $scope): array '', '', '', - '' ); + foreach ($this->crossCheckInterfacesHelper->check($classReflection) as $error) { + $implementsErrors[] = $error; + } + return array_merge($extendsErrors, $implementsErrors); } diff --git a/src/Rules/Generics/InterfaceTemplateTypeRule.php b/src/Rules/Generics/InterfaceTemplateTypeRule.php index 87f8b8cded..dd9198e96b 100644 --- a/src/Rules/Generics/InterfaceTemplateTypeRule.php +++ b/src/Rules/Generics/InterfaceTemplateTypeRule.php @@ -4,62 +4,50 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; +use PHPStan\Node\InClassNode; use PHPStan\Rules\Rule; -use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\TemplateTypeScope; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Interface_> + * @implements Rule */ class InterfaceTemplateTypeRule implements Rule { - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - private \PHPStan\Rules\Generics\TemplateTypeCheck $templateTypeCheck; - public function __construct( - FileTypeMapper $fileTypeMapper, - TemplateTypeCheck $templateTypeCheck + private TemplateTypeCheck $templateTypeCheck, ) { - $this->fileTypeMapper = $fileTypeMapper; - $this->templateTypeCheck = $templateTypeCheck; } public function getNodeType(): string { - return Node\Stmt\Interface_::class; + return InClassNode::class; } public function processNode(Node $node, Scope $scope): array { - $docComment = $node->getDocComment(); - if ($docComment === null) { + if (!$scope->isInClass()) { return []; } - - if (!isset($node->namespacedName)) { - throw new \PHPStan\ShouldNotHappenException(); + $classReflection = $scope->getClassReflection(); + if (!$classReflection->isInterface()) { + return []; } + $interfaceName = $classReflection->getName(); - $interfaceName = (string) $node->namespacedName; - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $scope->getFile(), - $interfaceName, - null, - null, - $docComment->getText() - ); + $escapadInterfaceName = SprintfHelper::escapeFormatString($interfaceName); return $this->templateTypeCheck->check( $node, TemplateTypeScope::createWithClass($interfaceName), - $resolvedPhpDoc->getTemplateTags(), - sprintf('PHPDoc tag @template for interface %s cannot have existing class %%s as its name.', $interfaceName), - sprintf('PHPDoc tag @template for interface %s cannot have existing type alias %%s as its name.', $interfaceName), - sprintf('PHPDoc tag @template %%s for interface %s has invalid bound type %%s.', $interfaceName), - sprintf('PHPDoc tag @template %%s for interface %s with bound type %%s is not supported.', $interfaceName) + $classReflection->getTemplateTags(), + sprintf('PHPDoc tag @template for interface %s cannot have existing class %%s as its name.', $escapadInterfaceName), + sprintf('PHPDoc tag @template for interface %s cannot have existing type alias %%s as its name.', $escapadInterfaceName), + sprintf('PHPDoc tag @template %%s for interface %s has invalid bound type %%s.', $escapadInterfaceName), + sprintf('PHPDoc tag @template %%s for interface %s with bound type %%s is not supported.', $escapadInterfaceName), ); } diff --git a/src/Rules/Generics/MethodSignatureVarianceRule.php b/src/Rules/Generics/MethodSignatureVarianceRule.php index 49e452476a..b2207734d9 100644 --- a/src/Rules/Generics/MethodSignatureVarianceRule.php +++ b/src/Rules/Generics/MethodSignatureVarianceRule.php @@ -4,22 +4,21 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InClassMethodNode; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ class MethodSignatureVarianceRule implements Rule { - private \PHPStan\Rules\Generics\VarianceCheck $varianceCheck; - - public function __construct(VarianceCheck $varianceCheck) + public function __construct(private VarianceCheck $varianceCheck) { - $this->varianceCheck = $varianceCheck; } public function getNodeType(): string @@ -36,10 +35,10 @@ public function processNode(Node $node, Scope $scope): array return $this->varianceCheck->checkParametersAcceptor( ParametersAcceptorSelector::selectSingle($method->getVariants()), - sprintf('in parameter %%s of method %s::%s()', $method->getDeclaringClass()->getDisplayName(), $method->getName()), + sprintf('in 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->getName() === '__construct' || $method->isStatic(), ); } diff --git a/src/Rules/Generics/MethodTemplateTypeRule.php b/src/Rules/Generics/MethodTemplateTypeRule.php index 57ac147a2c..7f51d9e0d5 100644 --- a/src/Rules/Generics/MethodTemplateTypeRule.php +++ b/src/Rules/Generics/MethodTemplateTypeRule.php @@ -4,29 +4,27 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\VerbosityLevel; +use function array_keys; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\ClassMethod> + * @implements Rule */ class MethodTemplateTypeRule implements Rule { - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - private \PHPStan\Rules\Generics\TemplateTypeCheck $templateTypeCheck; - public function __construct( - FileTypeMapper $fileTypeMapper, - TemplateTypeCheck $templateTypeCheck + private FileTypeMapper $fileTypeMapper, + private TemplateTypeCheck $templateTypeCheck, ) { - $this->fileTypeMapper = $fileTypeMapper; - $this->templateTypeCheck = $templateTypeCheck; } public function getNodeType(): string @@ -42,7 +40,7 @@ public function processNode(Node $node, Scope $scope): array } if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $classReflection = $scope->getClassReflection(); @@ -50,21 +48,23 @@ public function processNode(Node $node, Scope $scope): array $methodName = $node->name->toString(); $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( $scope->getFile(), - $className, + $classReflection->getName(), $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, $methodName, - $docComment->getText() + $docComment->getText(), ); $methodTemplateTags = $resolvedPhpDoc->getTemplateTags(); + $escapedClassName = SprintfHelper::escapeFormatString($className); + $escapedMethodName = SprintfHelper::escapeFormatString($methodName); $messages = $this->templateTypeCheck->check( $node, TemplateTypeScope::createWithMethod($className, $methodName), $methodTemplateTags, - sprintf('PHPDoc tag @template for method %s::%s() cannot have existing class %%s as its name.', $className, $methodName), - sprintf('PHPDoc tag @template for method %s::%s() cannot have existing type alias %%s as its name.', $className, $methodName), - sprintf('PHPDoc tag @template %%s for method %s::%s() has invalid bound type %%s.', $className, $methodName), - sprintf('PHPDoc tag @template %%s for method %s::%s() with bound type %%s is not supported.', $className, $methodName) + sprintf('PHPDoc tag @template for method %s::%s() cannot have existing class %%s as its name.', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @template for method %s::%s() cannot have existing type alias %%s as its name.', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @template %%s for method %s::%s() has invalid bound type %%s.', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @template %%s for method %s::%s() with bound type %%s is not supported.', $escapedClassName, $escapedMethodName), ); $classTemplateTypes = $classReflection->getTemplateTypeMap()->getTypes(); diff --git a/src/Rules/Generics/TemplateTypeCheck.php b/src/Rules/Generics/TemplateTypeCheck.php index 057623f0da..c8151a5129 100644 --- a/src/Rules/Generics/TemplateTypeCheck.php +++ b/src/Rules/Generics/TemplateTypeCheck.php @@ -3,10 +3,17 @@ namespace PHPStan\Rules\Generics; use PhpParser\Node; +use PHPStan\Internal\SprintfHelper; +use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Type\ArrayType; +use PHPStan\Type\BooleanType; +use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\FloatType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeScope; @@ -15,46 +22,30 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; -use PHPStan\Type\Type; use PHPStan\Type\TypeAliasResolver; -use PHPStan\Type\TypeTraverser; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use function array_map; +use function array_merge; +use function get_class; +use function sprintf; class TemplateTypeCheck { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private GenericObjectTypeCheck $genericObjectTypeCheck; - - private TypeAliasResolver $typeAliasResolver; - - private bool $checkClassCaseSensitivity; - public function __construct( - ReflectionProvider $reflectionProvider, - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - GenericObjectTypeCheck $genericObjectTypeCheck, - TypeAliasResolver $typeAliasResolver, - bool $checkClassCaseSensitivity + private ReflectionProvider $reflectionProvider, + private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private GenericObjectTypeCheck $genericObjectTypeCheck, + private TypeAliasResolver $typeAliasResolver, + private bool $checkClassCaseSensitivity, ) { - $this->reflectionProvider = $reflectionProvider; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->genericObjectTypeCheck = $genericObjectTypeCheck; - $this->typeAliasResolver = $typeAliasResolver; - $this->checkClassCaseSensitivity = $checkClassCaseSensitivity; } /** - * @param \PhpParser\Node $node - * @param TemplateTypeScope $templateTypeScope - * @param array $templateTags - * @return \PHPStan\Rules\RuleError[] + * @param array $templateTags + * @return RuleError[] */ public function check( Node $node, @@ -63,7 +54,7 @@ public function check( string $sameTemplateTypeNameAsClassMessage, string $sameTemplateTypeNameAsTypeMessage, string $invalidBoundTypeMessage, - string $notSupportedBoundMessage + string $notSupportedBoundMessage, ): array { $messages = []; @@ -72,13 +63,13 @@ public function check( if ($this->reflectionProvider->hasClass($templateTagName)) { $messages[] = RuleErrorBuilder::message(sprintf( $sameTemplateTypeNameAsClassMessage, - $templateTagName + $templateTagName, ))->build(); } if ($this->typeAliasResolver->hasTypeAlias($templateTagName, $templateTypeScope->getClassName())) { $messages[] = RuleErrorBuilder::message(sprintf( $sameTemplateTypeNameAsTypeMessage, - $templateTagName + $templateTagName, ))->build(); } $boundType = $templateTag->getBound(); @@ -93,43 +84,41 @@ public function check( $messages[] = RuleErrorBuilder::message(sprintf( $invalidBoundTypeMessage, $templateTagName, - $referencedClass + $referencedClass, ))->build(); } if ($this->checkClassCaseSensitivity) { - $classNameNodePairs = array_map(static function (string $referencedClass) use ($node): ClassNameNodePair { - return new ClassNameNodePair($referencedClass, $node); - }, $boundType->getReferencedClasses()); + $classNameNodePairs = array_map(static fn (string $referencedClass): ClassNameNodePair => new ClassNameNodePair($referencedClass, $node), $boundType->getReferencedClasses()); $messages = array_merge($messages, $this->classCaseSensitivityCheck->checkClassNames($classNameNodePairs)); } - TypeTraverser::map($templateTag->getBound(), static function (Type $type, callable $traverse) use (&$messages, $notSupportedBoundMessage, $templateTagName): Type { - $boundClass = get_class($type); - if ( - $boundClass === MixedType::class - || $boundClass === StringType::class - || $boundClass === IntegerType::class - || $boundClass === ObjectWithoutClassType::class - || $boundClass === ObjectType::class - || $boundClass === GenericObjectType::class - || $type instanceof UnionType - || $type instanceof TemplateType - ) { - return $traverse($type); - } - - $messages[] = RuleErrorBuilder::message(sprintf($notSupportedBoundMessage, $templateTagName, $type->describe(VerbosityLevel::typeOnly())))->build(); - - return $type; - }); + $boundType = $templateTag->getBound(); + $boundTypeClass = get_class($boundType); + if ( + $boundTypeClass !== MixedType::class + && $boundTypeClass !== ConstantArrayType::class + && $boundTypeClass !== ArrayType::class + && $boundTypeClass !== StringType::class + && $boundTypeClass !== IntegerType::class + && $boundTypeClass !== FloatType::class + && $boundTypeClass !== BooleanType::class + && $boundTypeClass !== ObjectWithoutClassType::class + && $boundTypeClass !== ObjectType::class + && $boundTypeClass !== GenericObjectType::class + && !$boundType instanceof UnionType + && !$boundType instanceof TemplateType + ) { + $messages[] = RuleErrorBuilder::message(sprintf($notSupportedBoundMessage, $templateTagName, $boundType->describe(VerbosityLevel::typeOnly())))->build(); + } + $escapedTemplateTagName = SprintfHelper::escapeFormatString($templateTagName); $genericObjectErrors = $this->genericObjectTypeCheck->check( $boundType, - sprintf('PHPDoc tag @template %s bound contains generic type %%s but class %%s is not generic.', $templateTagName), - sprintf('PHPDoc tag @template %s bound has type %%s which does not specify all template types of class %%s: %%s', $templateTagName), - sprintf('PHPDoc tag @template %s bound has type %%s which specifies %%d template types, but class %%s supports only %%d: %%s', $templateTagName), - sprintf('Type %%s in generic type %%s in PHPDoc tag @template %s is not subtype of template type %%s of class %%s.', $templateTagName) + sprintf('PHPDoc tag @template %s bound contains generic type %%s but %%s %%s is not generic.', $escapedTemplateTagName), + 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), ); foreach ($genericObjectErrors as $genericObjectError) { $messages[] = $genericObjectError; diff --git a/src/Rules/Generics/TraitTemplateTypeRule.php b/src/Rules/Generics/TraitTemplateTypeRule.php index 21b89339cd..5294ae9eaa 100644 --- a/src/Rules/Generics/TraitTemplateTypeRule.php +++ b/src/Rules/Generics/TraitTemplateTypeRule.php @@ -4,27 +4,24 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Rules\Rule; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\TemplateTypeScope; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Trait_> + * @implements Rule */ class TraitTemplateTypeRule implements Rule { - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - private \PHPStan\Rules\Generics\TemplateTypeCheck $templateTypeCheck; - public function __construct( - FileTypeMapper $fileTypeMapper, - TemplateTypeCheck $templateTypeCheck + private FileTypeMapper $fileTypeMapper, + private TemplateTypeCheck $templateTypeCheck, ) { - $this->fileTypeMapper = $fileTypeMapper; - $this->templateTypeCheck = $templateTypeCheck; } public function getNodeType(): string @@ -40,7 +37,7 @@ public function processNode(Node $node, Scope $scope): array } if (!isset($node->namespacedName)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $traitName = (string) $node->namespacedName; @@ -49,17 +46,19 @@ public function processNode(Node $node, Scope $scope): array $traitName, null, null, - $docComment->getText() + $docComment->getText(), ); + $escapedTraitName = SprintfHelper::escapeFormatString($traitName); + return $this->templateTypeCheck->check( $node, TemplateTypeScope::createWithClass($traitName), $resolvedPhpDoc->getTemplateTags(), - sprintf('PHPDoc tag @template for trait %s cannot have existing class %%s as its name.', $traitName), - sprintf('PHPDoc tag @template for trait %s cannot have existing type alias %%s as its name.', $traitName), - sprintf('PHPDoc tag @template %%s for trait %s has invalid bound type %%s.', $traitName), - sprintf('PHPDoc tag @template %%s for trait %s with bound type %%s is not supported.', $traitName) + sprintf('PHPDoc tag @template for trait %s cannot have existing class %%s as its name.', $escapedTraitName), + sprintf('PHPDoc tag @template for trait %s cannot have existing type alias %%s as its name.', $escapedTraitName), + sprintf('PHPDoc tag @template %%s for trait %s has invalid bound type %%s.', $escapedTraitName), + sprintf('PHPDoc tag @template %%s for trait %s with bound type %%s is not supported.', $escapedTraitName), ); } diff --git a/src/Rules/Generics/UsedTraitsRule.php b/src/Rules/Generics/UsedTraitsRule.php index 9e102bf341..fa8ec17a47 100644 --- a/src/Rules/Generics/UsedTraitsRule.php +++ b/src/Rules/Generics/UsedTraitsRule.php @@ -4,28 +4,27 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\PhpDoc\Tag\UsesTag; use PHPStan\Rules\Rule; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Type; +use function array_map; +use function sprintf; +use function ucfirst; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\TraitUse> + * @implements Rule */ class UsedTraitsRule implements Rule { - private \PHPStan\Type\FileTypeMapper $fileTypeMapper; - - private \PHPStan\Rules\Generics\GenericAncestorsCheck $genericAncestorsCheck; - public function __construct( - FileTypeMapper $fileTypeMapper, - GenericAncestorsCheck $genericAncestorsCheck + private FileTypeMapper $fileTypeMapper, + private GenericAncestorsCheck $genericAncestorsCheck, ) { - $this->fileTypeMapper = $fileTypeMapper; - $this->genericAncestorsCheck = $genericAncestorsCheck; } public function getNodeType(): string @@ -36,7 +35,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $className = $scope->getClassReflection()->getName(); @@ -52,33 +51,31 @@ public function processNode(Node $node, Scope $scope): array $className, $traitName, null, - $docComment->getText() + $docComment->getText(), ); $useTags = $resolvedPhpDoc->getUsesTags(); } - $description = sprintf('class %s', $className); + $description = sprintf('class %s', SprintfHelper::escapeFormatString($className)); $typeDescription = 'class'; if ($traitName !== null) { - $description = sprintf('trait %s', $traitName); + $description = sprintf('trait %s', SprintfHelper::escapeFormatString($traitName)); $typeDescription = 'trait'; } return $this->genericAncestorsCheck->check( $node->traits, - array_map(static function (UsesTag $tag): Type { - return $tag->getType(); - }, $useTags), + array_map(static fn (UsesTag $tag): Type => $tag->getType(), $useTags), sprintf('%s @use tag contains incompatible type %%s.', ucfirst($description)), sprintf('%s has @use tag, but does not use any trait.', ucfirst($description)), sprintf('The @use tag of %s describes %%s but the %s uses %%s.', $description, $typeDescription), - 'PHPDoc tag @use contains generic type %s but trait %s is not generic.', - 'Generic type %s in PHPDoc tag @use does not specify all template types of trait %s: %s', - 'Generic type %s in PHPDoc tag @use specifies %d template types, but trait %s supports only %d: %s', - 'Type %s in generic type %s in PHPDoc tag @use is not subtype of template type %s of trait %s.', + 'PHPDoc tag @use contains generic type %s but %s %s is not generic.', + '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.', '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) + sprintf('in used type %%s of %s', $description), ); } diff --git a/src/Rules/Generics/VarianceCheck.php b/src/Rules/Generics/VarianceCheck.php index e8a95fd3c3..34f2e4ffe6 100644 --- a/src/Rules/Generics/VarianceCheck.php +++ b/src/Rules/Generics/VarianceCheck.php @@ -8,6 +8,7 @@ use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Type; +use function sprintf; class VarianceCheck { @@ -18,7 +19,7 @@ public function checkParametersAcceptor( string $parameterTypeMessage, string $returnTypeMessage, string $generalMessage, - bool $isStatic + bool $isStatic, ): array { $errors = []; @@ -45,7 +46,7 @@ public function checkParametersAcceptor( $errors[] = RuleErrorBuilder::message(sprintf( 'Variance annotation is only allowed for type parameters of classes and interfaces, but occurs in template type %s in %s.', $templateType->getName(), - $generalMessage + $generalMessage, ))->build(); } @@ -75,7 +76,7 @@ public function check(TemplateTypeVariance $positionVariance, Type $type, string $referredType->getName(), $referredType->getVariance()->describe(), $reference->getPositionVariance()->describe(), - $messageContext + $messageContext, ))->build(); } diff --git a/src/Rules/IssetCheck.php b/src/Rules/IssetCheck.php index 6cfb74c7a9..6b5fec3210 100644 --- a/src/Rules/IssetCheck.php +++ b/src/Rules/IssetCheck.php @@ -8,52 +8,80 @@ use PHPStan\Rules\Properties\PropertyDescriptor; use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Type\MixedType; -use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function is_string; +use function sprintf; class IssetCheck { - private \PHPStan\Rules\Properties\PropertyDescriptor $propertyDescriptor; - - private \PHPStan\Rules\Properties\PropertyReflectionFinder $propertyReflectionFinder; - public function __construct( - PropertyDescriptor $propertyDescriptor, - PropertyReflectionFinder $propertyReflectionFinder + private PropertyDescriptor $propertyDescriptor, + private PropertyReflectionFinder $propertyReflectionFinder, + private bool $checkAdvancedIsset, + private bool $treatPhpDocTypesAsCertain, ) { - $this->propertyDescriptor = $propertyDescriptor; - $this->propertyReflectionFinder = $propertyReflectionFinder; } - public function check(Expr $expr, Scope $scope, string $operatorDescription, ?RuleError $error = null): ?RuleError + /** + * @param callable(Type): ?string $typeMessageCallback + */ + public function check(Expr $expr, Scope $scope, string $operatorDescription, callable $typeMessageCallback, ?RuleError $error = null): ?RuleError { + // mirrored in PHPStan\Analyser\MutatingScope::issetCheck() if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) { $hasVariable = $scope->hasVariableType($expr->name); if ($hasVariable->maybe()) { return null; } + if ($error === null) { + if ($hasVariable->yes()) { + if ($expr->name === '_SESSION') { + return null; + } + + return $this->generateError( + $scope->getVariableType($expr->name), + 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(); + } + return $error; } elseif ($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 $error; + return $error ?? $this->checkUndefined($expr->var, $scope, $operatorDescription); } if ($hasOffsetValue->no()) { - return $error ?? RuleErrorBuilder::message( + if ($error !== null) { + return $error; + } + + if (!$this->checkAdvancedIsset) { + return null; + } + + return RuleErrorBuilder::message( sprintf( 'Offset %s on %s %s does not exist.', $dimType->describe(VerbosityLevel::value()), $type->describe(VerbosityLevel::value()), - $operatorDescription - ) + $operatorDescription, + ), )->build(); } @@ -64,16 +92,23 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, ?Ru // If offset is 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()) { + if ($error !== null) { + return $error; + } + + if (!$this->checkAdvancedIsset) { + return null; + } - $error = $error ?? $this->generateError($type->getOffsetValueType($dimType), sprintf( + $error = $this->generateError($type->getOffsetValueType($dimType), sprintf( 'Offset %s on %s %s always exists and', $dimType->describe(VerbosityLevel::value()), $type->describe(VerbosityLevel::value()), - $operatorDescription - )); + $operatorDescription, + ), $typeMessageCallback); if ($error !== null) { - return $this->check($expr->var, $scope, $operatorDescription, $error); + return $this->check($expr->var, $scope, $operatorDescription, $typeMessageCallback, $error); } } @@ -85,61 +120,148 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, ?Ru $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $scope); if ($propertyReflection === null) { + if ($expr instanceof Node\Expr\PropertyFetch) { + return $this->checkUndefined($expr->var, $scope, $operatorDescription); + } + + if ($expr->class instanceof Expr) { + return $this->checkUndefined($expr->class, $scope, $operatorDescription); + } + return null; } if (!$propertyReflection->isNative()) { + if ($expr instanceof Node\Expr\PropertyFetch) { + return $this->checkUndefined($expr->var, $scope, $operatorDescription); + } + + if ($expr->class instanceof Expr) { + return $this->checkUndefined($expr->class, $scope, $operatorDescription); + } + return null; } $nativeType = $propertyReflection->getNativeType(); if (!$nativeType instanceof MixedType) { if (!$scope->isSpecified($expr)) { + if ($expr instanceof Node\Expr\PropertyFetch) { + return $this->checkUndefined($expr->var, $scope, $operatorDescription); + } + + if ($expr->class instanceof Expr) { + return $this->checkUndefined($expr->class, $scope, $operatorDescription); + } + return null; } } $propertyDescription = $this->propertyDescriptor->describeProperty($propertyReflection, $expr); $propertyType = $propertyReflection->getWritableType(); + if ($error !== null) { + return $error; + } + if (!$this->checkAdvancedIsset) { + if ($expr instanceof Node\Expr\PropertyFetch) { + return $this->checkUndefined($expr->var, $scope, $operatorDescription); + } + + if ($expr->class instanceof Expr) { + return $this->checkUndefined($expr->class, $scope, $operatorDescription); + } + + return null; + } - $error = $error ?? $this->generateError( + $error = $this->generateError( $propertyReflection->getWritableType(), - sprintf('%s (%s) %s', $propertyDescription, $propertyType->describe(VerbosityLevel::typeOnly()), $operatorDescription) + sprintf('%s (%s) %s', $propertyDescription, $propertyType->describe(VerbosityLevel::typeOnly()), $operatorDescription), + $typeMessageCallback, ); if ($error !== null) { if ($expr instanceof Node\Expr\PropertyFetch) { - return $this->check($expr->var, $scope, $operatorDescription, $error); + return $this->check($expr->var, $scope, $operatorDescription, $typeMessageCallback, $error); } if ($expr->class instanceof Expr) { - return $this->check($expr->class, $scope, $operatorDescription, $error); + return $this->check($expr->class, $scope, $operatorDescription, $typeMessageCallback, $error); } } return $error; } - return $error ?? $this->generateError($scope->getType($expr), sprintf('Expression %s', $operatorDescription)); + if ($error !== null) { + return $error; + } + + if (!$this->checkAdvancedIsset) { + return null; + } + + return $this->generateError($scope->getType($expr), sprintf('Expression %s', $operatorDescription), $typeMessageCallback); } - private function generateError(Type $type, string $message): ?RuleError + private function checkUndefined(Expr $expr, Scope $scope, string $operatorDescription): ?RuleError { - $nullType = new NullType(); + if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) { + $hasVariable = $scope->hasVariableType($expr->name); + if (!$hasVariable->no()) { + return null; + } - if ($type->equals($nullType)) { - return RuleErrorBuilder::message( - sprintf('%s is always null.', $message) - )->build(); + return RuleErrorBuilder::message(sprintf('Variable $%s %s is never defined.', $expr->name, $operatorDescription))->build(); } - if ($type->isSuperTypeOf($nullType)->no()) { + if ($expr instanceof Node\Expr\ArrayDimFetch && $expr->dim !== null) { + $type = $scope->getType($expr->var); + $dimType = $scope->getType($expr->dim); + $hasOffsetValue = $type->hasOffsetValueType($dimType); + if (!$type->isOffsetAccessible()->yes()) { + return $this->checkUndefined($expr->var, $scope, $operatorDescription); + } + + if (!$hasOffsetValue->no()) { + return $this->checkUndefined($expr->var, $scope, $operatorDescription); + } + return RuleErrorBuilder::message( - sprintf('%s is not nullable.', $message) + sprintf( + 'Offset %s on %s %s does not exist.', + $dimType->describe(VerbosityLevel::value()), + $type->describe(VerbosityLevel::value()), + $operatorDescription, + ), )->build(); } + if ($expr instanceof Expr\PropertyFetch) { + return $this->checkUndefined($expr->var, $scope, $operatorDescription); + } + + if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) { + return $this->checkUndefined($expr->class, $scope, $operatorDescription); + } + return null; } + /** + * @param callable(Type): ?string $typeMessageCallback + */ + private function generateError(Type $type, string $message, callable $typeMessageCallback): ?RuleError + { + $typeMessage = $typeMessageCallback($type); + if ($typeMessage === null) { + return null; + } + + return RuleErrorBuilder::message( + sprintf('%s %s.', $message, $typeMessage), + )->build(); + } + } diff --git a/src/Rules/Keywords/ContinueBreakInLoopRule.php b/src/Rules/Keywords/ContinueBreakInLoopRule.php index 987ae48f5e..0ccfea7527 100644 --- a/src/Rules/Keywords/ContinueBreakInLoopRule.php +++ b/src/Rules/Keywords/ContinueBreakInLoopRule.php @@ -7,6 +7,8 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; +use function sprintf; /** * @implements Rule @@ -42,7 +44,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message(sprintf( 'Keyword %s used outside of a loop or a switch statement.', - $node instanceof Stmt\Continue_ ? 'continue' : 'break' + $node instanceof Stmt\Continue_ ? 'continue' : 'break', ))->nonIgnorable()->build(), ]; } @@ -58,7 +60,7 @@ public function processNode(Node $node, Scope $scope): array $value--; $parent = $parent->getAttribute('parent'); if (!$parent instanceof Stmt\Switch_) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } } diff --git a/src/Rules/Methods/AbstractMethodInNonAbstractClassRule.php b/src/Rules/Methods/AbstractMethodInNonAbstractClassRule.php index d197147d5c..da1a3950b7 100644 --- a/src/Rules/Methods/AbstractMethodInNonAbstractClassRule.php +++ b/src/Rules/Methods/AbstractMethodInNonAbstractClassRule.php @@ -6,6 +6,8 @@ use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; +use function sprintf; /** * @implements Rule @@ -21,7 +23,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $class = $scope->getClassReflection(); diff --git a/src/Rules/Methods/CallMethodsRule.php b/src/Rules/Methods/CallMethodsRule.php index 1e901ce981..38b3769cd4 100644 --- a/src/Rules/Methods/CallMethodsRule.php +++ b/src/Rules/Methods/CallMethodsRule.php @@ -5,44 +5,23 @@ use PhpParser\Node; use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\FunctionCallParametersCheck; -use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Rules\RuleLevelHelper; -use PHPStan\Type\ErrorType; -use PHPStan\Type\Type; -use PHPStan\Type\VerbosityLevel; +use PHPStan\Rules\Rule; +use function array_merge; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\MethodCall> + * @implements Rule */ -class CallMethodsRule implements \PHPStan\Rules\Rule +class CallMethodsRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\FunctionCallParametersCheck $check; - - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - private bool $checkFunctionNameCase; - - private bool $reportMagicMethods; - public function __construct( - ReflectionProvider $reflectionProvider, - FunctionCallParametersCheck $check, - RuleLevelHelper $ruleLevelHelper, - bool $checkFunctionNameCase, - bool $reportMagicMethods + private MethodCallCheck $methodCallCheck, + private FunctionCallParametersCheck $parametersCheck, ) { - $this->reflectionProvider = $reflectionProvider; - $this->check = $check; - $this->ruleLevelHelper = $ruleLevelHelper; - $this->checkFunctionNameCase = $checkFunctionNameCase; - $this->reportMagicMethods = $reportMagicMethods; } public function getNodeType(): string @@ -56,90 +35,21 @@ public function processNode(Node $node, Scope $scope): array return []; } - $name = $node->name->name; - $typeResult = $this->ruleLevelHelper->findTypeToCheck( - $scope, - $node->var, - sprintf('Call to method %s() on an unknown class %%s.', $name), - static function (Type $type) use ($name): bool { - return $type->canCallMethods()->yes() && $type->hasMethod($name)->yes(); - } - ); - $type = $typeResult->getType(); - if ($type instanceof ErrorType) { - return $typeResult->getUnknownClassErrors(); - } - if (!$type->canCallMethods()->yes()) { - return [ - RuleErrorBuilder::message(sprintf( - 'Cannot call method %s() on %s.', - $name, - $type->describe(VerbosityLevel::typeOnly()) - ))->build(), - ]; - } - - if (!$type->hasMethod($name)->yes()) { - $directClassNames = $typeResult->getReferencedClasses(); - if (!$this->reportMagicMethods) { - foreach ($directClassNames as $className) { - if (!$this->reflectionProvider->hasClass($className)) { - continue; - } - - $classReflection = $this->reflectionProvider->getClass($className); - if ($classReflection->hasNativeMethod('__call')) { - return []; - } - } - } - - if (count($directClassNames) === 1) { - $referencedClass = $directClassNames[0]; - $methodClassReflection = $this->reflectionProvider->getClass($referencedClass); - $parentClassReflection = $methodClassReflection->getParentClass(); - while ($parentClassReflection !== false) { - if ($parentClassReflection->hasMethod($name)) { - return [ - RuleErrorBuilder::message(sprintf( - 'Call to private method %s() of parent class %s.', - $parentClassReflection->getMethod($name, $scope)->getName(), - $parentClassReflection->getDisplayName() - ))->build(), - ]; - } - - $parentClassReflection = $parentClassReflection->getParentClass(); - } - } + $methodName = $node->name->name; - return [ - RuleErrorBuilder::message(sprintf( - 'Call to an undefined method %s::%s().', - $type->describe(VerbosityLevel::typeOnly()), - $name - ))->build(), - ]; + [$errors, $methodReflection] = $this->methodCallCheck->check($scope, $methodName, $node->var); + if ($methodReflection === null) { + return $errors; } - $methodReflection = $type->getMethod($name, $scope); $declaringClass = $methodReflection->getDeclaringClass(); - $messagesMethodName = $declaringClass->getDisplayName() . '::' . $methodReflection->getName() . '()'; - $errors = []; - if (!$scope->canCallMethod($methodReflection)) { - $errors[] = RuleErrorBuilder::message(sprintf( - 'Call to %s method %s() of class %s.', - $methodReflection->isPrivate() ? 'private' : 'protected', - $methodReflection->getName(), - $declaringClass->getDisplayName() - ))->build(); - } + $messagesMethodName = SprintfHelper::escapeFormatString($declaringClass->getDisplayName() . '::' . $methodReflection->getName() . '()'); - $errors = array_merge($errors, $this->check->check( + return array_merge($errors, $this->parametersCheck->check( ParametersAcceptorSelector::selectFromArgs( $scope, - $node->args, - $methodReflection->getVariants() + $node->getArgs(), + $methodReflection->getVariants(), ), $scope, $declaringClass->isBuiltin(), @@ -158,20 +68,8 @@ static function (Type $type) use ($name): bool { 'Missing parameter $%s in call to method ' . $messagesMethodName . '.', 'Unknown parameter $%s in call to method ' . $messagesMethodName . '.', 'Return type of call to method ' . $messagesMethodName . ' contains unresolvable type.', - ] + ], )); - - if ( - $this->checkFunctionNameCase - && strtolower($methodReflection->getName()) === strtolower($name) - && $methodReflection->getName() !== $name - ) { - $errors[] = RuleErrorBuilder::message( - sprintf('Call to method %s with incorrect case: %s', $messagesMethodName, $name) - )->build(); - } - - return $errors; } } diff --git a/src/Rules/Methods/CallPrivateMethodThroughStaticRule.php b/src/Rules/Methods/CallPrivateMethodThroughStaticRule.php new file mode 100644 index 0000000000..8fd13a19e5 --- /dev/null +++ b/src/Rules/Methods/CallPrivateMethodThroughStaticRule.php @@ -0,0 +1,62 @@ + + */ +class CallPrivateMethodThroughStaticRule implements Rule +{ + + public function getNodeType(): string + { + return StaticCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Identifier) { + return []; + } + if (!$node->class instanceof Name) { + return []; + } + + $methodName = $node->name->name; + $className = $node->class; + if ($className->toLowerString() !== 'static') { + return []; + } + + $classType = $scope->resolveTypeByName($className); + if (!$classType->hasMethod($methodName)->yes()) { + return []; + } + + $method = $classType->getMethod($methodName, $scope); + if (!$method->isPrivate()) { + return []; + } + + if ($scope->isInClass() && $scope->getClassReflection()->isFinal()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Unsafe call to private method %s::%s() through static::.', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + ))->build(), + ]; + } + +} diff --git a/src/Rules/Methods/CallStaticMethodsRule.php b/src/Rules/Methods/CallStaticMethodsRule.php index 12d0d16271..c53822e7a2 100644 --- a/src/Rules/Methods/CallStaticMethodsRule.php +++ b/src/Rules/Methods/CallStaticMethodsRule.php @@ -4,62 +4,25 @@ use PhpParser\Node; use PhpParser\Node\Expr\StaticCall; -use PhpParser\Node\Name; use PHPStan\Analyser\Scope; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\Native\NativeMethodReflection; +use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\Php\PhpMethodReflection; -use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Rules\ClassCaseSensitivityCheck; -use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\FunctionCallParametersCheck; -use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\Rules\RuleLevelHelper; -use PHPStan\Type\ErrorType; -use PHPStan\Type\Generic\GenericClassStringType; -use PHPStan\Type\ObjectWithoutClassType; -use PHPStan\Type\StringType; -use PHPStan\Type\ThisType; -use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; -use PHPStan\Type\TypeWithClassName; -use PHPStan\Type\VerbosityLevel; +use PHPStan\Rules\Rule; +use function array_merge; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\StaticCall> + * @implements Rule */ -class CallStaticMethodsRule implements \PHPStan\Rules\Rule +class CallStaticMethodsRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\FunctionCallParametersCheck $check; - - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private bool $checkFunctionNameCase; - - private bool $reportMagicMethods; - public function __construct( - ReflectionProvider $reflectionProvider, - FunctionCallParametersCheck $check, - RuleLevelHelper $ruleLevelHelper, - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - bool $checkFunctionNameCase, - bool $reportMagicMethods + private StaticMethodCallCheck $methodCallCheck, + private FunctionCallParametersCheck $parametersCheck, ) { - $this->reflectionProvider = $reflectionProvider; - $this->check = $check; - $this->ruleLevelHelper = $ruleLevelHelper; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->checkFunctionNameCase = $checkFunctionNameCase; - $this->reportMagicMethods = $reportMagicMethods; } public function getNodeType(): string @@ -74,204 +37,27 @@ public function processNode(Node $node, Scope $scope): array } $methodName = $node->name->name; - $class = $node->class; - $errors = []; - $isAbstract = false; - if ($class instanceof Name) { - $className = (string) $class; - $lowercasedClassName = strtolower($className); - if (in_array($lowercasedClassName, ['self', 'static'], true)) { - if (!$scope->isInClass()) { - return [ - RuleErrorBuilder::message(sprintf( - 'Calling %s::%s() outside of class scope.', - $className, - $methodName - ))->build(), - ]; - } - $classType = $scope->resolveTypeByName($class); - } elseif ($lowercasedClassName === 'parent') { - if (!$scope->isInClass()) { - return [ - RuleErrorBuilder::message(sprintf( - 'Calling %s::%s() outside of class scope.', - $className, - $methodName - ))->build(), - ]; - } - $currentClassReflection = $scope->getClassReflection(); - if ($currentClassReflection->getParentClass() === false) { - return [ - RuleErrorBuilder::message(sprintf( - '%s::%s() calls parent::%s() but %s does not extend any class.', - $scope->getClassReflection()->getDisplayName(), - $scope->getFunctionName(), - $methodName, - $scope->getClassReflection()->getDisplayName() - ))->build(), - ]; - } - - if ($scope->getFunctionName() === null) { - throw new \PHPStan\ShouldNotHappenException(); - } - - $classType = $scope->resolveTypeByName($class); - } else { - if (!$this->reflectionProvider->hasClass($className)) { - if ($scope->isInClassExists($className)) { - return []; - } - - return [ - RuleErrorBuilder::message(sprintf( - 'Call to static method %s() on an unknown class %s.', - $methodName, - $className - ))->discoveringSymbolsTip()->build(), - ]; - } else { - $errors = $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($className, $class)]); - } - - $classType = $scope->resolveTypeByName($class); - } - - $classReflection = $classType->getClassReflection(); - if ($classReflection !== null && $classReflection->hasNativeMethod($methodName) && $lowercasedClassName !== 'static') { - $nativeMethodReflection = $classReflection->getNativeMethod($methodName); - if ($nativeMethodReflection instanceof PhpMethodReflection || $nativeMethodReflection instanceof NativeMethodReflection) { - $isAbstract = $nativeMethodReflection->isAbstract(); - } - } - } else { - $classTypeResult = $this->ruleLevelHelper->findTypeToCheck( - $scope, - $class, - sprintf('Call to static method %s() on an unknown class %%s.', $methodName), - static function (Type $type) use ($methodName): bool { - return $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(); - } - ); - $classType = $classTypeResult->getType(); - if ($classType instanceof ErrorType) { - return $classTypeResult->getUnknownClassErrors(); - } - } - - if ($classType instanceof GenericClassStringType) { - $classType = $classType->getGenericType(); - if (!(new ObjectWithoutClassType())->isSuperTypeOf($classType)->yes()) { - return []; - } - } elseif ((new StringType())->isSuperTypeOf($classType)->yes()) { - return []; - } - - $typeForDescribe = $classType; - if ($classType instanceof ThisType) { - $typeForDescribe = $classType->getStaticObjectType(); - } - $classType = TypeCombinator::remove($classType, new StringType()); - - if (!$classType->canCallMethods()->yes()) { - return array_merge($errors, [ - RuleErrorBuilder::message(sprintf( - 'Cannot call static method %s() on %s.', - $methodName, - $typeForDescribe->describe(VerbosityLevel::typeOnly()) - ))->build(), - ]); + [$errors, $method] = $this->methodCallCheck->check($scope, $methodName, $node->class); + if ($method === null) { + return $errors; } - if (!$classType->hasMethod($methodName)->yes()) { - if (!$this->reportMagicMethods) { - $directClassNames = TypeUtils::getDirectClassNames($classType); - foreach ($directClassNames as $className) { - if (!$this->reflectionProvider->hasClass($className)) { - continue; - } - - $classReflection = $this->reflectionProvider->getClass($className); - if ($classReflection->hasNativeMethod('__callStatic')) { - return []; - } - } - } - - return array_merge($errors, [ - RuleErrorBuilder::message(sprintf( - 'Call to an undefined static method %s::%s().', - $typeForDescribe->describe(VerbosityLevel::typeOnly()), - $methodName - ))->build(), - ]); - } - - $method = $classType->getMethod($methodName, $scope); - if (!$method->isStatic()) { - $function = $scope->getFunction(); - if ( - !$function instanceof MethodReflection - || $function->isStatic() - || !$scope->isInClass() - || ( - $classType instanceof TypeWithClassName - && $scope->getClassReflection()->getName() !== $classType->getClassName() - && !$scope->getClassReflection()->isSubclassOf($classType->getClassName()) - ) - ) { - return array_merge($errors, [ - RuleErrorBuilder::message(sprintf( - 'Static call to instance method %s::%s().', - $method->getDeclaringClass()->getDisplayName(), - $method->getName() - ))->build(), - ]); - } - } - - if (!$scope->canCallMethod($method)) { - $errors = array_merge($errors, [ - RuleErrorBuilder::message(sprintf( - 'Call to %s %s %s() of class %s.', - $method->isPrivate() ? 'private' : 'protected', - $method->isStatic() ? 'static method' : 'method', - $method->getName(), - $method->getDeclaringClass()->getDisplayName() - ))->build(), - ]); - } - - if ($isAbstract) { - return [ - RuleErrorBuilder::message(sprintf( - 'Cannot call abstract%s method %s::%s().', - $method->isStatic() ? ' static' : '', - $method->getDeclaringClass()->getDisplayName(), - $method->getName() - ))->build(), - ]; - } - - $lowercasedMethodName = sprintf( - '%s %s', - $method->isStatic() ? 'static method' : 'method', - $method->getDeclaringClass()->getDisplayName() . '::' . $method->getName() . '()' - ); - $displayMethodName = sprintf( + $displayMethodName = SprintfHelper::escapeFormatString(sprintf( '%s %s', $method->isStatic() ? 'Static method' : 'Method', - $method->getDeclaringClass()->getDisplayName() . '::' . $method->getName() . '()' - ); + $method->getDeclaringClass()->getDisplayName() . '::' . $method->getName() . '()', + )); + $lowercasedMethodName = SprintfHelper::escapeFormatString(sprintf( + '%s %s', + $method->isStatic() ? 'static method' : 'method', + $method->getDeclaringClass()->getDisplayName() . '::' . $method->getName() . '()', + )); - $errors = array_merge($errors, $this->check->check( + $errors = array_merge($errors, $this->parametersCheck->check( ParametersAcceptorSelector::selectFromArgs( $scope, - $node->args, - $method->getVariants() + $node->getArgs(), + $method->getVariants(), ), $scope, $method->getDeclaringClass()->isBuiltin(), @@ -290,20 +76,9 @@ static function (Type $type) use ($methodName): bool { 'Missing parameter $%s in call to ' . $lowercasedMethodName . '.', 'Unknown parameter $%s in call to ' . $lowercasedMethodName . '.', 'Return type of call to ' . $lowercasedMethodName . ' contains unresolvable type.', - ] + ], )); - if ( - $this->checkFunctionNameCase - && $method->getName() !== $methodName - ) { - $errors[] = RuleErrorBuilder::message(sprintf( - 'Call to %s with incorrect case: %s', - $lowercasedMethodName, - $methodName - ))->build(); - } - return $errors; } diff --git a/src/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRule.php b/src/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRule.php index a42bc59fa5..713f3996af 100644 --- a/src/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRule.php +++ b/src/Rules/Methods/CallToConstructorStatementWithoutSideEffectsRule.php @@ -9,18 +9,16 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\NeverType; use PHPStan\Type\VoidType; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Expression> + * @implements Rule */ class CallToConstructorStatementWithoutSideEffectsRule implements Rule { - private ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct(private ReflectionProvider $reflectionProvider) { - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -65,7 +63,7 @@ public function processNode(Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'Call to %s::%s() on a separate line has no effect.', $classReflection->getDisplayName(), - $constructor->getName() + $constructor->getName(), ))->build(), ]; } diff --git a/src/Rules/Methods/CallToMethodStamentWithoutSideEffectsRule.php b/src/Rules/Methods/CallToMethodStatementWithoutSideEffectsRule.php similarity index 70% rename from src/Rules/Methods/CallToMethodStamentWithoutSideEffectsRule.php rename to src/Rules/Methods/CallToMethodStatementWithoutSideEffectsRule.php index 7592c9bd18..93b9202984 100644 --- a/src/Rules/Methods/CallToMethodStamentWithoutSideEffectsRule.php +++ b/src/Rules/Methods/CallToMethodStatementWithoutSideEffectsRule.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Methods; use PhpParser\Node; +use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -11,18 +12,16 @@ use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\VoidType; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Expression> + * @implements Rule */ -class CallToMethodStamentWithoutSideEffectsRule implements Rule +class CallToMethodStatementWithoutSideEffectsRule implements Rule { - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - public function __construct(RuleLevelHelper $ruleLevelHelper) + public function __construct(private RuleLevelHelper $ruleLevelHelper) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string @@ -46,11 +45,9 @@ public function processNode(Node $node, Scope $scope): array $typeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, - $methodCall->var, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $methodCall->var), '', - static function (Type $type) use ($methodName): bool { - return $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(); - } + static fn (Type $type): bool => $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(), ); $calledOnType = $typeResult->getType(); if ($calledOnType instanceof ErrorType) { @@ -65,10 +62,12 @@ static function (Type $type) use ($methodName): bool { } $method = $calledOnType->getMethod($methodName, $scope); - if ($method->hasSideEffects()->no()) { - $throwsType = $method->getThrowType(); - if ($throwsType !== null && !$throwsType instanceof VoidType) { - return []; + if ($method->hasSideEffects()->no() || $node->expr->isFirstClassCallable()) { + if (!$node->expr->isFirstClassCallable()) { + $throwsType = $method->getThrowType(); + if ($throwsType !== null && !$throwsType instanceof VoidType) { + return []; + } } $methodResult = $scope->getType($methodCall); @@ -81,7 +80,7 @@ static function (Type $type) use ($methodName): bool { 'Call to %s %s::%s() on a separate line has no effect.', $method->isStatic() ? 'static method' : 'method', $method->getDeclaringClass()->getDisplayName(), - $method->getName() + $method->getName(), ))->build(), ]; } diff --git a/src/Rules/Methods/CallToStaticMethodStamentWithoutSideEffectsRule.php b/src/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRule.php similarity index 72% rename from src/Rules/Methods/CallToStaticMethodStamentWithoutSideEffectsRule.php rename to src/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRule.php index 85d8a47a3b..593846a1d2 100644 --- a/src/Rules/Methods/CallToStaticMethodStamentWithoutSideEffectsRule.php +++ b/src/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRule.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Methods; use PhpParser\Node; +use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; @@ -13,24 +14,20 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\VoidType; +use function sprintf; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Expression> + * @implements Rule */ -class CallToStaticMethodStamentWithoutSideEffectsRule implements Rule +class CallToStaticMethodStatementWithoutSideEffectsRule implements Rule { - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - public function __construct( - RuleLevelHelper $ruleLevelHelper, - ReflectionProvider $reflectionProvider + private RuleLevelHelper $ruleLevelHelper, + private ReflectionProvider $reflectionProvider, ) { - $this->ruleLevelHelper = $ruleLevelHelper; - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -60,11 +57,9 @@ public function processNode(Node $node, Scope $scope): array } else { $typeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, - $staticCall->class, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $staticCall->class), '', - static function (Type $type) use ($methodName): bool { - return $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(); - } + static fn (Type $type): bool => $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(), ); $calledOnType = $typeResult->getType(); if ($calledOnType instanceof ErrorType) { @@ -90,10 +85,12 @@ static function (Type $type) use ($methodName): bool { return []; } - if ($method->hasSideEffects()->no()) { - $throwsType = $method->getThrowType(); - if ($throwsType !== null && !$throwsType instanceof VoidType) { - return []; + if ($method->hasSideEffects()->no() || $node->expr->isFirstClassCallable()) { + if (!$node->expr->isFirstClassCallable()) { + $throwsType = $method->getThrowType(); + if ($throwsType !== null && !$throwsType instanceof VoidType) { + return []; + } } $methodResult = $scope->getType($staticCall); @@ -106,7 +103,7 @@ static function (Type $type) use ($methodName): bool { 'Call to %s %s::%s() on a separate line has no effect.', $method->isStatic() ? 'static method' : 'method', $method->getDeclaringClass()->getDisplayName(), - $method->getName() + $method->getName(), ))->build(), ]; } diff --git a/src/Rules/Methods/ExistingClassesInTypehintsRule.php b/src/Rules/Methods/ExistingClassesInTypehintsRule.php index 18d088b5a6..0f3ee88ed5 100644 --- a/src/Rules/Methods/ExistingClassesInTypehintsRule.php +++ b/src/Rules/Methods/ExistingClassesInTypehintsRule.php @@ -4,21 +4,22 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InClassMethodNode; use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Rules\FunctionDefinitionCheck; +use PHPStan\Rules\Rule; +use PHPStan\ShouldNotHappenException; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\InClassMethodNode> + * @implements Rule */ -class ExistingClassesInTypehintsRule implements \PHPStan\Rules\Rule +class ExistingClassesInTypehintsRule implements Rule { - private \PHPStan\Rules\FunctionDefinitionCheck $check; - - public function __construct(FunctionDefinitionCheck $check) + public function __construct(private FunctionDefinitionCheck $check) { - $this->check = $check; } public function getNodeType(): string @@ -30,27 +31,40 @@ public function processNode(Node $node, Scope $scope): array { $methodReflection = $scope->getFunction(); if (!$methodReflection instanceof PhpMethodFromParserNodeReflection) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } + $className = SprintfHelper::escapeFormatString($scope->getClassReflection()->getDisplayName()); + $methodName = SprintfHelper::escapeFormatString($methodReflection->getName()); + return $this->check->checkClassMethod( $methodReflection, $node->getOriginalNode(), sprintf( - 'Parameter $%%s of method %s::%s() has invalid typehint type %%s.', - $scope->getClassReflection()->getDisplayName(), - $methodReflection->getName() + 'Parameter $%%s of method %s::%s() has invalid type %%s.', + $className, + $methodName, + ), + sprintf( + 'Method %s::%s() has invalid return type %%s.', + $className, + $methodName, + ), + sprintf('Method %s::%s() uses native union types but they\'re supported only on PHP 8.0 and later.', $className, $methodName), + sprintf('Template type %%s of method %s::%s() is not referenced in a parameter.', $className, $methodName), + sprintf( + 'Parameter $%%s of method %s::%s() has unresolvable native type.', + $className, + $methodName, ), sprintf( - 'Return typehint of method %s::%s() has invalid type %%s.', - $scope->getClassReflection()->getDisplayName(), - $methodReflection->getName() + 'Method %s::%s() has unresolvable native return type.', + $className, + $methodName, ), - sprintf('Method %s::%s() uses native union types but they\'re supported only on PHP 8.0 and later.', $scope->getClassReflection()->getDisplayName(), $methodReflection->getName()), - sprintf('Template type %%s of method %s::%s() is not referenced in a parameter.', $scope->getClassReflection()->getDisplayName(), $methodReflection->getName()) ); } diff --git a/src/Rules/Methods/IncompatibleDefaultParameterTypeRule.php b/src/Rules/Methods/IncompatibleDefaultParameterTypeRule.php index 172b0170ee..19aa3e2947 100644 --- a/src/Rules/Methods/IncompatibleDefaultParameterTypeRule.php +++ b/src/Rules/Methods/IncompatibleDefaultParameterTypeRule.php @@ -9,11 +9,14 @@ use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\VerbosityLevel; +use function is_string; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\InClassMethodNode> + * @implements Rule */ class IncompatibleDefaultParameterTypeRule implements Rule { @@ -41,7 +44,7 @@ public function processNode(Node $node, Scope $scope): array $param->var instanceof Node\Expr\Error || !is_string($param->var->name) ) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $defaultValueType = $scope->getType($param->default); @@ -61,7 +64,7 @@ public function processNode(Node $node, Scope $scope): array $defaultValueType->describe($verbosityLevel), $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $parameterType->describe($verbosityLevel) + $parameterType->describe($verbosityLevel), ))->line($param->getLine())->build(); } diff --git a/src/Rules/Methods/MethodAttributesRule.php b/src/Rules/Methods/MethodAttributesRule.php index 5e501f76d5..ae220df96e 100644 --- a/src/Rules/Methods/MethodAttributesRule.php +++ b/src/Rules/Methods/MethodAttributesRule.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Methods; +use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Rules\AttributesCheck; @@ -13,11 +14,8 @@ class MethodAttributesRule implements Rule { - private AttributesCheck $attributesCheck; - - public function __construct(AttributesCheck $attributesCheck) + public function __construct(private AttributesCheck $attributesCheck) { - $this->attributesCheck = $attributesCheck; } public function getNodeType(): string @@ -30,8 +28,8 @@ public function processNode(Node $node, Scope $scope): array return $this->attributesCheck->check( $scope, $node->attrGroups, - \Attribute::TARGET_METHOD, - 'method' + Attribute::TARGET_METHOD, + 'method', ); } diff --git a/src/Rules/Methods/MethodCallCheck.php b/src/Rules/Methods/MethodCallCheck.php new file mode 100644 index 0000000000..bd46e50708 --- /dev/null +++ b/src/Rules/Methods/MethodCallCheck.php @@ -0,0 +1,142 @@ +ruleLevelHelper->findTypeToCheck( + $scope, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $var), + sprintf('Call to method %s() on an unknown class %%s.', SprintfHelper::escapeFormatString($methodName)), + static fn (Type $type): bool => $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(), + ); + + $type = $typeResult->getType(); + if ($type instanceof ErrorType) { + return [$typeResult->getUnknownClassErrors(), null]; + } + if (!$type->canCallMethods()->yes()) { + return [ + [ + RuleErrorBuilder::message(sprintf( + 'Cannot call method %s() on %s.', + $methodName, + $type->describe(VerbosityLevel::typeOnly()), + ))->build(), + ], + null, + ]; + } + + if (!$type->hasMethod($methodName)->yes()) { + $directClassNames = $typeResult->getReferencedClasses(); + if (!$this->reportMagicMethods) { + foreach ($directClassNames as $className) { + if (!$this->reflectionProvider->hasClass($className)) { + continue; + } + + $classReflection = $this->reflectionProvider->getClass($className); + if ($classReflection->hasNativeMethod('__call')) { + return [[], null]; + } + } + } + + if (count($directClassNames) === 1) { + $referencedClass = $directClassNames[0]; + $methodClassReflection = $this->reflectionProvider->getClass($referencedClass); + $parentClassReflection = $methodClassReflection->getParentClass(); + while ($parentClassReflection !== null) { + if ($parentClassReflection->hasMethod($methodName)) { + $methodReflection = $parentClassReflection->getMethod($methodName, $scope); + return [ + [ + RuleErrorBuilder::message(sprintf( + 'Call to private method %s() of parent class %s.', + $methodReflection->getName(), + $parentClassReflection->getDisplayName(), + ))->build(), + ], + $methodReflection, + ]; + } + + $parentClassReflection = $parentClassReflection->getParentClass(); + } + } + + return [ + [ + RuleErrorBuilder::message(sprintf( + 'Call to an undefined method %s::%s().', + $type->describe(VerbosityLevel::typeOnly()), + $methodName, + ))->build(), + ], + null, + ]; + } + + $methodReflection = $type->getMethod($methodName, $scope); + $declaringClass = $methodReflection->getDeclaringClass(); + $messagesMethodName = SprintfHelper::escapeFormatString($declaringClass->getDisplayName() . '::' . $methodReflection->getName() . '()'); + $errors = []; + if (!$scope->canCallMethod($methodReflection)) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Call to %s method %s() of class %s.', + $methodReflection->isPrivate() ? 'private' : 'protected', + $methodReflection->getName(), + $declaringClass->getDisplayName(), + ))->build(); + } + + if ( + $this->checkFunctionNameCase + && strtolower($methodReflection->getName()) === strtolower($methodName) + && $methodReflection->getName() !== $methodName + ) { + $errors[] = RuleErrorBuilder::message( + sprintf('Call to method %s with incorrect case: %s', $messagesMethodName, $methodName), + )->build(); + } + + return [$errors, $methodReflection]; + } + +} diff --git a/src/Rules/Methods/MethodCallableRule.php b/src/Rules/Methods/MethodCallableRule.php new file mode 100644 index 0000000000..8cd5c3f14b --- /dev/null +++ b/src/Rules/Methods/MethodCallableRule.php @@ -0,0 +1,63 @@ + + */ +class MethodCallableRule implements Rule +{ + + public function __construct(private MethodCallCheck $methodCallCheck, private PhpVersion $phpVersion) + { + } + + public function getNodeType(): string + { + return MethodCallableNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$this->phpVersion->supportsFirstClassCallables()) { + return [ + RuleErrorBuilder::message('First-class callables are supported only on PHP 8.1 and later.') + ->nonIgnorable() + ->build(), + ]; + } + + $methodName = $node->getName(); + if (!$methodName instanceof Node\Identifier) { + return []; + } + + $methodNameName = $methodName->toString(); + + [$errors, $methodReflection] = $this->methodCallCheck->check($scope, $methodNameName, $node->getVar()); + if ($methodReflection === null) { + return $errors; + } + + $declaringClass = $methodReflection->getDeclaringClass(); + if ($declaringClass->hasNativeMethod($methodNameName)) { + return $errors; + } + + $messagesMethodName = SprintfHelper::escapeFormatString($declaringClass->getDisplayName() . '::' . $methodReflection->getName() . '()'); + + $errors[] = RuleErrorBuilder::message(sprintf('Creating callable from a non-native method %s.', $messagesMethodName))->build(); + + return $errors; + } + +} diff --git a/src/Rules/Methods/MethodSignatureRule.php b/src/Rules/Methods/MethodSignatureRule.php index 2599c46ea2..a02e974f4c 100644 --- a/src/Rules/Methods/MethodSignatureRule.php +++ b/src/Rules/Methods/MethodSignatureRule.php @@ -6,9 +6,12 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\InClassMethodNode; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParameterReflectionWithPhpDocs; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ParametersAcceptorWithPhpDocs; use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeHelper; @@ -20,24 +23,21 @@ use PHPStan\Type\TypeTraverser; use PHPStan\Type\VerbosityLevel; use PHPStan\Type\VoidType; +use function count; +use function min; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -class MethodSignatureRule implements \PHPStan\Rules\Rule +class MethodSignatureRule implements Rule { - private bool $reportMaybes; - - private bool $reportStatic; - public function __construct( - bool $reportMaybes, - bool $reportStatic + private bool $reportMaybes, + private bool $reportStatic, ) { - $this->reportMaybes = $reportMaybes; - $this->reportStatic = $reportStatic; } public function getNodeType(): string @@ -86,7 +86,7 @@ public function processNode(Node $node, Scope $scope): array $returnTypeCompatibility->no() ? 'compatible' : 'covariant', $parentReturnType->describe(VerbosityLevel::value()), $parentMethod->getDeclaringClass()->getDisplayName(), - $parentMethod->getName() + $parentMethod->getName(), ))->build(); } @@ -111,7 +111,7 @@ public function processNode(Node $node, Scope $scope): array $parentParameter->getName(), $parentParameterType->describe(VerbosityLevel::value()), $parentMethod->getDeclaringClass()->getDisplayName(), - $parentMethod->getName() + $parentMethod->getName(), ))->build(); } } @@ -120,16 +120,14 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param string $methodName - * @param \PHPStan\Reflection\ClassReflection $class - * @return \PHPStan\Reflection\MethodReflection[] + * @return MethodReflection[] */ private function collectParentMethods(string $methodName, ClassReflection $class): array { $parentMethods = []; $parentClass = $class->getParentClass(); - if ($parentClass !== false && $parentClass->hasNativeMethod($methodName)) { + if ($parentClass !== null && $parentClass->hasNativeMethod($methodName)) { $parentMethod = $parentClass->getNativeMethod($methodName); if (!$parentMethod->isPrivate()) { $parentMethods[] = $parentMethod; @@ -148,23 +146,21 @@ private function collectParentMethods(string $methodName, ClassReflection $class } /** - * @param ParametersAcceptorWithPhpDocs $currentVariant - * @param ParametersAcceptorWithPhpDocs $parentVariant * @return array{TrinaryLogic, Type, Type} */ private function checkReturnTypeCompatibility( ClassReflection $declaringClass, ParametersAcceptorWithPhpDocs $currentVariant, - ParametersAcceptorWithPhpDocs $parentVariant + ParametersAcceptorWithPhpDocs $parentVariant, ): array { $returnType = TypehintHelper::decideType( $currentVariant->getNativeReturnType(), - TemplateTypeHelper::resolveToBounds($currentVariant->getPhpDocReturnType()) + TemplateTypeHelper::resolveToBounds($currentVariant->getPhpDocReturnType()), ); $originalParentReturnType = TypehintHelper::decideType( $parentVariant->getNativeReturnType(), - TemplateTypeHelper::resolveToBounds($parentVariant->getPhpDocReturnType()) + TemplateTypeHelper::resolveToBounds($parentVariant->getPhpDocReturnType()), ); $parentReturnType = $this->transformStaticType($declaringClass, $originalParentReturnType); // Allow adding `void` return type hints when the parent defines no return type @@ -179,19 +175,19 @@ private function checkReturnTypeCompatibility( return [$parentReturnType->isSuperTypeOf($returnType), TypehintHelper::decideType( $currentVariant->getNativeReturnType(), - $currentVariant->getPhpDocReturnType() + $currentVariant->getPhpDocReturnType(), ), $originalParentReturnType]; } /** - * @param \PHPStan\Reflection\ParameterReflectionWithPhpDocs[] $parameters - * @param \PHPStan\Reflection\ParameterReflectionWithPhpDocs[] $parentParameters + * @param ParameterReflectionWithPhpDocs[] $parameters + * @param ParameterReflectionWithPhpDocs[] $parentParameters * @return array */ private function checkParameterTypeCompatibility( ClassReflection $declaringClass, array $parameters, - array $parentParameters + array $parentParameters, ): array { $parameterResults = []; @@ -203,17 +199,17 @@ private function checkParameterTypeCompatibility( $parameterType = TypehintHelper::decideType( $parameter->getNativeType(), - TemplateTypeHelper::resolveToBounds($parameter->getPhpDocType()) + TemplateTypeHelper::resolveToBounds($parameter->getPhpDocType()), ); $originalParameterType = TypehintHelper::decideType( $parentParameter->getNativeType(), - TemplateTypeHelper::resolveToBounds($parentParameter->getPhpDocType()) + TemplateTypeHelper::resolveToBounds($parentParameter->getPhpDocType()), ); $parentParameterType = $this->transformStaticType($declaringClass, $originalParameterType); $parameterResults[] = [$parameterType->isSuperTypeOf($parentParameterType), TypehintHelper::decideType( $parameter->getNativeType(), - $parameter->getPhpDocType() + $parameter->getPhpDocType(), ), $originalParameterType]; } diff --git a/src/Rules/Methods/MissingMethodImplementationRule.php b/src/Rules/Methods/MissingMethodImplementationRule.php index 201ce41461..4ccab0d1e1 100644 --- a/src/Rules/Methods/MissingMethodImplementationRule.php +++ b/src/Rules/Methods/MissingMethodImplementationRule.php @@ -8,6 +8,7 @@ use PHPStan\Node\InClassNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function sprintf; /** * @implements Rule @@ -34,7 +35,7 @@ public function processNode(Node $node, Scope $scope): array try { $nativeMethods = $classReflection->getNativeReflection()->getMethods(); - } catch (IdentifierNotFound $e) { + } catch (IdentifierNotFound) { return []; } foreach ($nativeMethods as $method) { @@ -44,12 +45,18 @@ public function processNode(Node $node, Scope $scope): array $declaringClass = $method->getDeclaringClass(); + $classLikeDescription = 'Non-abstract class'; + if ($classReflection->isEnum()) { + $classLikeDescription = 'Enum'; + } + $messages[] = RuleErrorBuilder::message(sprintf( - 'Non-abstract class %s contains abstract method %s() from %s %s.', + '%s %s contains abstract method %s() from %s %s.', + $classLikeDescription, $classReflection->getDisplayName(), $method->getName(), $declaringClass->isInterface() ? 'interface' : 'class', - $declaringClass->getName() + $declaringClass->getName(), ))->nonIgnorable()->build(); } diff --git a/src/Rules/Methods/MissingMethodParameterTypehintRule.php b/src/Rules/Methods/MissingMethodParameterTypehintRule.php index 13a7c2983a..b94ccdb832 100644 --- a/src/Rules/Methods/MissingMethodParameterTypehintRule.php +++ b/src/Rules/Methods/MissingMethodParameterTypehintRule.php @@ -9,21 +9,22 @@ use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\MissingTypehintCheck; +use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; use PHPStan\Type\VerbosityLevel; +use function implode; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -final class MissingMethodParameterTypehintRule implements \PHPStan\Rules\Rule +final class MissingMethodParameterTypehintRule implements Rule { - private \PHPStan\Rules\MissingTypehintCheck $missingTypehintCheck; - - public function __construct(MissingTypehintCheck $missingTypehintCheck) + public function __construct(private MissingTypehintCheck $missingTypehintCheck) { - $this->missingTypehintCheck = $missingTypehintCheck; } public function getNodeType(): string @@ -50,9 +51,7 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param \PHPStan\Reflection\MethodReflection $methodReflection - * @param \PHPStan\Reflection\ParameterReflection $parameterReflection - * @return \PHPStan\Rules\RuleError[] + * @return RuleError[] */ private function checkMethodParameter(MethodReflection $methodReflection, ParameterReflection $parameterReflection): array { @@ -61,10 +60,10 @@ private function checkMethodParameter(MethodReflection $methodReflection, Parame if ($parameterType instanceof MixedType && !$parameterType->isExplicitMixed()) { return [ RuleErrorBuilder::message(sprintf( - 'Method %s::%s() has parameter $%s with no typehint specified.', + 'Method %s::%s() has parameter $%s with no type specified.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $parameterReflection->getName() + $parameterReflection->getName(), ))->build(), ]; } @@ -77,7 +76,7 @@ private function checkMethodParameter(MethodReflection $methodReflection, Parame $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), $parameterReflection->getName(), - $iterableTypeDescription + $iterableTypeDescription, ))->tip(MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP)->build(); } @@ -88,7 +87,7 @@ private function checkMethodParameter(MethodReflection $methodReflection, Parame $methodReflection->getName(), $parameterReflection->getName(), $name, - implode(', ', $genericTypeNames) + implode(', ', $genericTypeNames), ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); } @@ -98,7 +97,7 @@ private function checkMethodParameter(MethodReflection $methodReflection, Parame $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), $parameterReflection->getName(), - $callableType->describe(VerbosityLevel::typeOnly()) + $callableType->describe(VerbosityLevel::typeOnly()), ))->build(); } diff --git a/src/Rules/Methods/MissingMethodReturnTypehintRule.php b/src/Rules/Methods/MissingMethodReturnTypehintRule.php index c8779f0e2a..7b891b9f62 100644 --- a/src/Rules/Methods/MissingMethodReturnTypehintRule.php +++ b/src/Rules/Methods/MissingMethodReturnTypehintRule.php @@ -8,21 +8,21 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\MissingTypehintCheck; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; use PHPStan\Type\VerbosityLevel; +use function implode; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -final class MissingMethodReturnTypehintRule implements \PHPStan\Rules\Rule +final class MissingMethodReturnTypehintRule implements Rule { - private \PHPStan\Rules\MissingTypehintCheck $missingTypehintCheck; - - public function __construct(MissingTypehintCheck $missingTypehintCheck) + public function __construct(private MissingTypehintCheck $missingTypehintCheck) { - $this->missingTypehintCheck = $missingTypehintCheck; } public function getNodeType(): string @@ -42,9 +42,9 @@ public function processNode(Node $node, Scope $scope): array if ($returnType instanceof MixedType && !$returnType->isExplicitMixed()) { return [ RuleErrorBuilder::message(sprintf( - 'Method %s::%s() has no return typehint specified.', + 'Method %s::%s() has no return type specified.', $methodReflection->getDeclaringClass()->getDisplayName(), - $methodReflection->getName() + $methodReflection->getName(), ))->build(), ]; } @@ -56,7 +56,7 @@ public function processNode(Node $node, Scope $scope): array 'Method %s::%s() return type has no value type specified in iterable type %s.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $iterableTypeDescription + $iterableTypeDescription, ))->tip(MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP)->build(); } @@ -66,7 +66,7 @@ public function processNode(Node $node, Scope $scope): array $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), $name, - implode(', ', $genericTypeNames) + implode(', ', $genericTypeNames), ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); } @@ -75,7 +75,7 @@ public function processNode(Node $node, Scope $scope): array 'Method %s::%s() return type has no signature specified for %s.', $methodReflection->getDeclaringClass()->getDisplayName(), $methodReflection->getName(), - $callableType->describe(VerbosityLevel::typeOnly()) + $callableType->describe(VerbosityLevel::typeOnly()), ))->build(); } diff --git a/src/Rules/Methods/NullsafeMethodCallRule.php b/src/Rules/Methods/NullsafeMethodCallRule.php index a70bf220f2..d060485df7 100644 --- a/src/Rules/Methods/NullsafeMethodCallRule.php +++ b/src/Rules/Methods/NullsafeMethodCallRule.php @@ -8,6 +8,7 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\NullType; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** * @implements Rule diff --git a/src/Rules/Methods/OverridingMethodRule.php b/src/Rules/Methods/OverridingMethodRule.php index 059c21df82..3d38e95a22 100644 --- a/src/Rules/Methods/OverridingMethodRule.php +++ b/src/Rules/Methods/OverridingMethodRule.php @@ -14,6 +14,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\ArrayType; use PHPStan\Type\IterableType; use PHPStan\Type\MixedType; @@ -21,7 +22,12 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; +use Traversable; +use function array_key_exists; use function array_slice; +use function count; +use function sprintf; +use function strtolower; /** * @implements Rule @@ -29,21 +35,12 @@ class OverridingMethodRule implements Rule { - private PhpVersion $phpVersion; - - private MethodSignatureRule $methodSignatureRule; - - private bool $checkPhpDocMethodSignatures; - public function __construct( - PhpVersion $phpVersion, - MethodSignatureRule $methodSignatureRule, - bool $checkPhpDocMethodSignatures + private PhpVersion $phpVersion, + private MethodSignatureRule $methodSignatureRule, + private bool $checkPhpDocMethodSignatures, ) { - $this->phpVersion = $phpVersion; - $this->methodSignatureRule = $methodSignatureRule; - $this->checkPhpDocMethodSignatures = $checkPhpDocMethodSignatures; } public function getNodeType(): string @@ -55,14 +52,14 @@ public function processNode(Node $node, Scope $scope): array { $method = $scope->getFunction(); if (!$method instanceof PhpMethodFromParserNodeReflection) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $prototype = $method->getPrototype(); if ($prototype->getDeclaringClass()->getName() === $method->getDeclaringClass()->getName()) { if (strtolower($method->getName()) === '__construct') { $parent = $method->getDeclaringClass()->getParentClass(); - if ($parent !== false && $parent->hasConstructor()) { + if ($parent !== null && $parent->hasConstructor()) { $parentConstructor = $parent->getConstructor(); if ($parentConstructor->isFinal()->yes()) { return $this->addErrors([ @@ -71,7 +68,7 @@ public function processNode(Node $node, Scope $scope): array $method->getDeclaringClass()->getDisplayName(), $method->getName(), $parent->getDisplayName(), - $parentConstructor->getName() + $parentConstructor->getName(), ))->nonIgnorable()->build(), ], $node, $scope); } @@ -92,7 +89,7 @@ public function processNode(Node $node, Scope $scope): array $method->getDeclaringClass()->getDisplayName(), $method->getName(), $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() + $prototype->getName(), ))->nonIgnorable()->build(); } @@ -103,7 +100,7 @@ public function processNode(Node $node, Scope $scope): array $method->getDeclaringClass()->getDisplayName(), $method->getName(), $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() + $prototype->getName(), ))->nonIgnorable()->build(); } } elseif ($method->isStatic()) { @@ -112,7 +109,7 @@ public function processNode(Node $node, Scope $scope): array $method->getDeclaringClass()->getDisplayName(), $method->getName(), $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() + $prototype->getName(), ))->nonIgnorable()->build(); } @@ -124,7 +121,7 @@ public function processNode(Node $node, Scope $scope): array $method->getDeclaringClass()->getDisplayName(), $method->getName(), $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() + $prototype->getName(), ))->nonIgnorable()->build(); } } elseif ($method->isPrivate()) { @@ -133,7 +130,7 @@ public function processNode(Node $node, Scope $scope): array $method->getDeclaringClass()->getDisplayName(), $method->getName(), $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() + $prototype->getName(), ))->nonIgnorable()->build(); } @@ -145,8 +142,28 @@ public function processNode(Node $node, Scope $scope): array $prototypeVariant = $prototypeVariants[0]; $methodVariant = ParametersAcceptorSelector::selectSingle($method->getVariants()); + $methodReturnType = $methodVariant->getNativeReturnType(); $methodParameters = $methodVariant->getParameters(); + if ( + $this->phpVersion->hasTentativeReturnTypes() + && $prototype->getTentativeReturnType() !== null + && !$this->hasReturnTypeWillChangeAttribute($node->getOriginalNode()) + ) { + + if (!$this->isTypeCompatible($prototype->getTentativeReturnType(), $methodVariant->getNativeReturnType(), true)) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Return type %s of method %s::%s() is not covariant with tentative return type %s of method %s::%s().', + $methodReturnType->describe(VerbosityLevel::typeOnly()), + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $prototype->getTentativeReturnType()->describe(VerbosityLevel::typeOnly()), + $prototype->getDeclaringClass()->getDisplayName(), + $prototype->getName(), + ))->tip('Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.')->nonIgnorable()->build(); + } + } + $prototypeAfterVariadic = false; foreach ($prototypeVariant->getParameters() as $i => $prototypeParameter) { if (!array_key_exists($i, $methodParameters)) { @@ -157,7 +174,7 @@ public function processNode(Node $node, Scope $scope): array $prototype->getDeclaringClass()->getDisplayName(), $prototype->getName(), $i + 1, - $prototypeParameter->getName() + $prototypeParameter->getName(), ))->nonIgnorable()->build(); continue; } @@ -174,7 +191,7 @@ public function processNode(Node $node, Scope $scope): array $i + 1, $prototypeParameter->getName(), $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() + $prototype->getName(), ))->nonIgnorable()->build(); } } elseif ($methodParameter->passedByReference()->no()) { @@ -187,7 +204,7 @@ public function processNode(Node $node, Scope $scope): array $i + 1, $prototypeParameter->getName(), $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() + $prototype->getName(), ))->nonIgnorable()->build(); } @@ -201,7 +218,7 @@ public function processNode(Node $node, Scope $scope): array $i + 1, $methodParameter->getName(), $method->getDeclaringClass()->getDisplayName(), - $method->getName() + $method->getName(), ))->nonIgnorable()->build(); continue; } @@ -215,7 +232,7 @@ public function processNode(Node $node, Scope $scope): array $i + 1, $prototypeParameter->getName(), $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() + $prototype->getName(), ))->nonIgnorable()->build(); continue; } elseif (count($methodParameters) === $i + 1) { @@ -224,7 +241,7 @@ public function processNode(Node $node, Scope $scope): array $i + 1, $methodParameter->getName(), $method->getDeclaringClass()->getDisplayName(), - $method->getName() + $method->getName(), ))->nonIgnorable()->build(); } } @@ -250,7 +267,7 @@ public function processNode(Node $node, Scope $scope): array $remainingPrototypeParameter->getName(), $remainingPrototypeParameter->getNativeType()->describe(VerbosityLevel::typeOnly()), $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() + $prototype->getName(), ))->nonIgnorable()->build(); } break; @@ -264,7 +281,7 @@ public function processNode(Node $node, Scope $scope): array $i + 1, $prototypeParameter->getName(), $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() + $prototype->getName(), ))->nonIgnorable()->build(); continue; } @@ -279,7 +296,7 @@ public function processNode(Node $node, Scope $scope): array $i + 1, $prototypeParameter->getName(), $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() + $prototype->getName(), ))->nonIgnorable()->build(); } @@ -303,7 +320,7 @@ public function processNode(Node $node, Scope $scope): array $prototypeParameter->getName(), $prototypeParameterType->describe(VerbosityLevel::typeOnly()), $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() + $prototype->getName(), ))->nonIgnorable()->build(); } continue; @@ -325,7 +342,7 @@ public function processNode(Node $node, Scope $scope): array $prototypeParameter->getName(), $prototypeParameterType->describe(VerbosityLevel::typeOnly()), $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() + $prototype->getName(), ))->nonIgnorable()->build(); } else { $messages[] = RuleErrorBuilder::message(sprintf( @@ -339,7 +356,7 @@ public function processNode(Node $node, Scope $scope): array $prototypeParameter->getName(), $prototypeParameterType->describe(VerbosityLevel::typeOnly()), $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() + $prototype->getName(), ))->nonIgnorable()->build(); } } @@ -363,7 +380,7 @@ public function processNode(Node $node, Scope $scope): array $j + 1, $methodParameter->getName(), $method->getDeclaringClass()->getDisplayName(), - $method->getName() + $method->getName(), ))->nonIgnorable()->build(); continue; } @@ -374,14 +391,12 @@ public function processNode(Node $node, Scope $scope): array $j + 1, $methodParameter->getName(), $method->getDeclaringClass()->getDisplayName(), - $method->getName() + $method->getName(), ))->nonIgnorable()->build(); continue; } } - $methodReturnType = $methodVariant->getNativeReturnType(); - if (!$prototypeVariant instanceof FunctionVariantWithPhpDocs) { return $this->addErrors($messages, $node, $scope); } @@ -397,7 +412,7 @@ public function processNode(Node $node, Scope $scope): array $method->getName(), $prototypeReturnType->describe(VerbosityLevel::typeOnly()), $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() + $prototype->getName(), ))->nonIgnorable()->build(); } else { $messages[] = RuleErrorBuilder::message(sprintf( @@ -407,7 +422,7 @@ public function processNode(Node $node, Scope $scope): array $method->getName(), $prototypeReturnType->describe(VerbosityLevel::typeOnly()), $prototype->getDeclaringClass()->getDisplayName(), - $prototype->getName() + $prototype->getName(), ))->nonIgnorable()->build(); } } @@ -434,7 +449,7 @@ private function isTypeCompatible(Type $methodParameterType, Type $prototypePara if ($prototypeParameterType instanceof ArrayType) { return true; } - if ($prototypeParameterType instanceof ObjectType && $prototypeParameterType->getClassName() === \Traversable::class) { + if ($prototypeParameterType instanceof ObjectType && $prototypeParameterType->getClassName() === Traversable::class) { return true; } } @@ -452,7 +467,7 @@ private function isTypeCompatible(Type $methodParameterType, Type $prototypePara private function addErrors( array $errors, InClassMethodNode $classMethod, - Scope $scope + Scope $scope, ): array { if (count($errors) > 0) { @@ -466,4 +481,17 @@ private function addErrors( return $this->methodSignatureRule->processNode($classMethod, $scope); } + private function hasReturnTypeWillChangeAttribute(Node\Stmt\ClassMethod $method): bool + { + foreach ($method->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + if ($attr->name->toLowerString() === 'returntypewillchange') { + return true; + } + } + } + + return false; + } + } diff --git a/src/Rules/Methods/ReturnTypeRule.php b/src/Rules/Methods/ReturnTypeRule.php index 451eb96a26..e1fae76875 100644 --- a/src/Rules/Methods/ReturnTypeRule.php +++ b/src/Rules/Methods/ReturnTypeRule.php @@ -5,21 +5,20 @@ use PhpParser\Node; use PhpParser\Node\Stmt\Return_; use PHPStan\Analyser\Scope; -use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Rules\FunctionReturnTypeCheck; +use PHPStan\Rules\Rule; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Return_> + * @implements Rule */ -class ReturnTypeRule implements \PHPStan\Rules\Rule +class ReturnTypeRule implements Rule { - private \PHPStan\Rules\FunctionReturnTypeCheck $returnTypeCheck; - - public function __construct(FunctionReturnTypeCheck $returnTypeCheck) + public function __construct(private FunctionReturnTypeCheck $returnTypeCheck) { - $this->returnTypeCheck = $returnTypeCheck; } public function getNodeType(): string @@ -38,15 +37,10 @@ public function processNode(Node $node, Scope $scope): array } $method = $scope->getFunction(); - if (!($method instanceof MethodReflection)) { + if (!$method instanceof PhpMethodFromParserNodeReflection) { return []; } - $reflection = null; - if ($method->getDeclaringClass()->getNativeReflection()->hasMethod($method->getName())) { - $reflection = $method->getDeclaringClass()->getNativeReflection()->getMethod($method->getName()); - } - return $this->returnTypeCheck->checkReturnType( $scope, ParametersAcceptorSelector::selectSingle($method->getVariants())->getReturnType(), @@ -55,24 +49,24 @@ public function processNode(Node $node, Scope $scope): array sprintf( 'Method %s::%s() should return %%s but empty return statement found.', $method->getDeclaringClass()->getDisplayName(), - $method->getName() + $method->getName(), ), sprintf( 'Method %s::%s() with return type void returns %%s but should not return anything.', $method->getDeclaringClass()->getDisplayName(), - $method->getName() + $method->getName(), ), sprintf( 'Method %s::%s() should return %%s but returns %%s.', $method->getDeclaringClass()->getDisplayName(), - $method->getName() + $method->getName(), ), sprintf( 'Method %s::%s() should never return but return statement found.', $method->getDeclaringClass()->getDisplayName(), - $method->getName() + $method->getName(), ), - $reflection !== null && $reflection->isGenerator() + $method->isGenerator(), ); } diff --git a/src/Rules/Methods/StaticMethodCallCheck.php b/src/Rules/Methods/StaticMethodCallCheck.php new file mode 100644 index 0000000000..e4d4ae017e --- /dev/null +++ b/src/Rules/Methods/StaticMethodCallCheck.php @@ -0,0 +1,280 @@ +isInClass()) { + return [ + [ + RuleErrorBuilder::message(sprintf( + 'Calling %s::%s() outside of class scope.', + $className, + $methodName, + ))->build(), + ], + null, + ]; + } + $classType = $scope->resolveTypeByName($class); + } elseif ($lowercasedClassName === 'parent') { + if (!$scope->isInClass()) { + return [ + [ + RuleErrorBuilder::message(sprintf( + 'Calling %s::%s() outside of class scope.', + $className, + $methodName, + ))->build(), + ], + null, + ]; + } + $currentClassReflection = $scope->getClassReflection(); + if ($currentClassReflection->getParentClass() === null) { + return [ + [ + RuleErrorBuilder::message(sprintf( + '%s::%s() calls parent::%s() but %s does not extend any class.', + $scope->getClassReflection()->getDisplayName(), + $scope->getFunctionName(), + $methodName, + $scope->getClassReflection()->getDisplayName(), + ))->build(), + ], + null, + ]; + } + + if ($scope->getFunctionName() === null) { + throw new ShouldNotHappenException(); + } + + $classType = $scope->resolveTypeByName($class); + } else { + if (!$this->reflectionProvider->hasClass($className)) { + if ($scope->isInClassExists($className)) { + return [[], null]; + } + + return [ + [ + RuleErrorBuilder::message(sprintf( + 'Call to static method %s() on an unknown class %s.', + $methodName, + $className, + ))->discoveringSymbolsTip()->build(), + ], + null, + ]; + } else { + $errors = $this->classCaseSensitivityCheck->checkClassNames([new ClassNameNodePair($className, $class)]); + } + + $classType = $scope->resolveTypeByName($class); + } + + $classReflection = $classType->getClassReflection(); + if ($classReflection !== null && $classReflection->hasNativeMethod($methodName) && $lowercasedClassName !== 'static') { + $nativeMethodReflection = $classReflection->getNativeMethod($methodName); + if ($nativeMethodReflection instanceof PhpMethodReflection || $nativeMethodReflection instanceof NativeMethodReflection) { + $isAbstract = $nativeMethodReflection->isAbstract(); + } + } + } else { + $classTypeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $class), + sprintf('Call to static method %s() on an unknown class %%s.', SprintfHelper::escapeFormatString($methodName)), + static fn (Type $type): bool => $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(), + ); + $classType = $classTypeResult->getType(); + if ($classType instanceof ErrorType) { + return [$classTypeResult->getUnknownClassErrors(), null]; + } + } + + if ($classType instanceof GenericClassStringType) { + $classType = $classType->getGenericType(); + if (!(new ObjectWithoutClassType())->isSuperTypeOf($classType)->yes()) { + return [[], null]; + } + } elseif ((new StringType())->isSuperTypeOf($classType)->yes()) { + return [[], null]; + } + + $typeForDescribe = $classType; + if ($classType instanceof ThisType) { + $typeForDescribe = $classType->getStaticObjectType(); + } + $classType = TypeCombinator::remove($classType, new StringType()); + + if (!$classType->canCallMethods()->yes()) { + return [ + array_merge($errors, [ + RuleErrorBuilder::message(sprintf( + 'Cannot call static method %s() on %s.', + $methodName, + $typeForDescribe->describe(VerbosityLevel::typeOnly()), + ))->build(), + ]), + null, + ]; + } + + if (!$classType->hasMethod($methodName)->yes()) { + if (!$this->reportMagicMethods) { + $directClassNames = TypeUtils::getDirectClassNames($classType); + foreach ($directClassNames as $className) { + if (!$this->reflectionProvider->hasClass($className)) { + continue; + } + + $classReflection = $this->reflectionProvider->getClass($className); + if ($classReflection->hasNativeMethod('__callStatic')) { + return [[], null]; + } + } + } + + return [ + array_merge($errors, [ + RuleErrorBuilder::message(sprintf( + 'Call to an undefined static method %s::%s().', + $typeForDescribe->describe(VerbosityLevel::typeOnly()), + $methodName, + ))->build(), + ]), + null, + ]; + } + + $method = $classType->getMethod($methodName, $scope); + if (!$method->isStatic()) { + $function = $scope->getFunction(); + if ( + !$function instanceof MethodReflection + || $function->isStatic() + || !$scope->isInClass() + || ( + $classType instanceof TypeWithClassName + && $scope->getClassReflection()->getName() !== $classType->getClassName() + && !$scope->getClassReflection()->isSubclassOf($classType->getClassName()) + ) + ) { + return [ + array_merge($errors, [ + RuleErrorBuilder::message(sprintf( + 'Static call to instance method %s::%s().', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + ))->build(), + ]), + $method, + ]; + } + } + + if (!$scope->canCallMethod($method)) { + $errors = array_merge($errors, [ + RuleErrorBuilder::message(sprintf( + 'Call to %s %s %s() of class %s.', + $method->isPrivate() ? 'private' : 'protected', + $method->isStatic() ? 'static method' : 'method', + $method->getName(), + $method->getDeclaringClass()->getDisplayName(), + ))->build(), + ]); + } + + if ($isAbstract) { + return [ + [ + RuleErrorBuilder::message(sprintf( + 'Cannot call abstract%s method %s::%s().', + $method->isStatic() ? ' static' : '', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + ))->build(), + ], + $method, + ]; + } + + $lowercasedMethodName = SprintfHelper::escapeFormatString(sprintf( + '%s %s', + $method->isStatic() ? 'static method' : 'method', + $method->getDeclaringClass()->getDisplayName() . '::' . $method->getName() . '()', + )); + + if ( + $this->checkFunctionNameCase + && $method->getName() !== $methodName + ) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Call to %s with incorrect case: %s', + $lowercasedMethodName, + $methodName, + ))->build(); + } + + return [$errors, $method]; + } + +} diff --git a/src/Rules/Methods/StaticMethodCallableRule.php b/src/Rules/Methods/StaticMethodCallableRule.php new file mode 100644 index 0000000000..72ddeda460 --- /dev/null +++ b/src/Rules/Methods/StaticMethodCallableRule.php @@ -0,0 +1,63 @@ + + */ +class StaticMethodCallableRule implements Rule +{ + + public function __construct(private StaticMethodCallCheck $methodCallCheck, private PhpVersion $phpVersion) + { + } + + public function getNodeType(): string + { + return StaticMethodCallableNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$this->phpVersion->supportsFirstClassCallables()) { + return [ + RuleErrorBuilder::message('First-class callables are supported only on PHP 8.1 and later.') + ->nonIgnorable() + ->build(), + ]; + } + + $methodName = $node->getName(); + if (!$methodName instanceof Node\Identifier) { + return []; + } + + $methodNameName = $methodName->toString(); + + [$errors, $methodReflection] = $this->methodCallCheck->check($scope, $methodNameName, $node->getClass()); + if ($methodReflection === null) { + return $errors; + } + + $declaringClass = $methodReflection->getDeclaringClass(); + if ($declaringClass->hasNativeMethod($methodNameName)) { + return $errors; + } + + $messagesMethodName = SprintfHelper::escapeFormatString($declaringClass->getDisplayName() . '::' . $methodReflection->getName() . '()'); + + $errors[] = RuleErrorBuilder::message(sprintf('Creating callable from a non-native static method %s.', $messagesMethodName))->build(); + + return $errors; + } + +} diff --git a/src/Rules/Missing/MissingClosureNativeReturnTypehintRule.php b/src/Rules/Missing/MissingClosureNativeReturnTypehintRule.php deleted file mode 100644 index b9154d40c2..0000000000 --- a/src/Rules/Missing/MissingClosureNativeReturnTypehintRule.php +++ /dev/null @@ -1,136 +0,0 @@ - - */ -class MissingClosureNativeReturnTypehintRule implements Rule -{ - - private bool $checkObjectTypehint; - - public function __construct(bool $checkObjectTypehint) - { - $this->checkObjectTypehint = $checkObjectTypehint; - } - - public function getNodeType(): string - { - return ClosureReturnStatementsNode::class; - } - - public function processNode(Node $node, Scope $scope): array - { - $closure = $node->getClosureExpr(); - if ($closure->returnType !== null) { - return []; - } - - $messagePattern = 'Anonymous function should have native return typehint "%s".'; - $statementResult = $node->getStatementResult(); - if ($statementResult->hasYield()) { - return [ - RuleErrorBuilder::message(sprintf($messagePattern, 'Generator'))->build(), - ]; - } - - $returnStatements = $node->getReturnStatements(); - if (count($returnStatements) === 0) { - return [ - RuleErrorBuilder::message(sprintf($messagePattern, 'void'))->build(), - ]; - } - - $returnTypes = []; - $voidReturnNodes = []; - $hasNull = false; - foreach ($returnStatements as $returnStatement) { - $returnNode = $returnStatement->getReturnNode(); - if ($returnNode->expr === null) { - $voidReturnNodes[] = $returnNode; - $hasNull = true; - continue; - } - - $returnTypes[] = $returnStatement->getScope()->getType($returnNode->expr); - } - - if (count($returnTypes) === 0) { - return [ - RuleErrorBuilder::message(sprintf($messagePattern, 'void'))->build(), - ]; - } - - $messages = []; - foreach ($voidReturnNodes as $voidReturnStatement) { - $messages[] = RuleErrorBuilder::message('Mixing returning values with empty return statements - return null should be used here.') - ->line($voidReturnStatement->getLine()) - ->build(); - } - - $returnType = TypeCombinator::union(...$returnTypes); - if ( - $returnType instanceof MixedType - || $returnType instanceof NeverType - || $returnType instanceof IntersectionType - || $returnType instanceof NullType - ) { - return $messages; - } - - if (TypeCombinator::containsNull($returnType)) { - $hasNull = true; - $returnType = TypeCombinator::removeNull($returnType); - } - - if ( - $returnType instanceof UnionType - || $returnType instanceof ResourceType - ) { - return $messages; - } - - if (!$statementResult->isAlwaysTerminating()) { - $messages[] = RuleErrorBuilder::message('Anonymous function sometimes return something but return statement at the end is missing.')->build(); - return $messages; - } - - $returnType = TypeUtils::generalizeType($returnType); - $description = $returnType->describe(VerbosityLevel::typeOnly()); - if ($returnType->isArray()->yes()) { - $description = 'array'; - } - if ($hasNull) { - $description = '?' . $description; - } - - if ( - !$this->checkObjectTypehint - && $returnType instanceof ObjectWithoutClassType - ) { - return $messages; - } - - $messages[] = RuleErrorBuilder::message(sprintf($messagePattern, $description))->build(); - - return $messages; - } - -} diff --git a/src/Rules/Missing/MissingReturnRule.php b/src/Rules/Missing/MissingReturnRule.php index 7074f4fb88..e7109da073 100644 --- a/src/Rules/Missing/MissingReturnRule.php +++ b/src/Rules/Missing/MissingReturnRule.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Missing; +use Generator; use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\ExecutionEndNode; @@ -9,31 +10,28 @@ use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Generic\TemplateMixedType; use PHPStan\Type\GenericTypeVariableResolver; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeWithClassName; use PHPStan\Type\VerbosityLevel; use PHPStan\Type\VoidType; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\ExecutionEndNode> + * @implements Rule */ class MissingReturnRule implements Rule { - private bool $checkExplicitMixedMissingReturn; - - private bool $checkPhpDocMissingReturn; - public function __construct( - bool $checkExplicitMixedMissingReturn, - bool $checkPhpDocMissingReturn + private bool $checkExplicitMixedMissingReturn, + private bool $checkPhpDocMissingReturn, ) { - $this->checkExplicitMixedMissingReturn = $checkExplicitMixedMissingReturn; - $this->checkPhpDocMissingReturn = $checkPhpDocMissingReturn; } public function getNodeType(): string @@ -64,7 +62,7 @@ public function processNode(Node $node, Scope $scope): array $description = sprintf('Function %s()', $scopeFunction->getName()); } } else { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $isVoidSuperType = $returnType->isSuperTypeOf(new VoidType()); @@ -76,8 +74,8 @@ public function processNode(Node $node, Scope $scope): array if ($returnType instanceof TypeWithClassName && $this->checkPhpDocMissingReturn) { $generatorReturnType = GenericTypeVariableResolver::getType( $returnType, - \Generator::class, - 'TReturn' + Generator::class, + 'TReturn', ); if ($generatorReturnType !== null) { $returnType = $generatorReturnType; @@ -87,7 +85,7 @@ public function processNode(Node $node, Scope $scope): array if (!$returnType instanceof MixedType) { return [ RuleErrorBuilder::message( - sprintf('%s should return %s but return statement is missing.', $description, $returnType->describe(VerbosityLevel::typeOnly())) + sprintf('%s should return %s but return statement is missing.', $description, $returnType->describe(VerbosityLevel::typeOnly())), )->line($node->getNode()->getStartLine())->build(), ]; } @@ -96,19 +94,30 @@ public function processNode(Node $node, Scope $scope): array return []; } - if (!$node->hasNativeReturnTypehint() && !$this->checkPhpDocMissingReturn) { + if ( + !$node->hasNativeReturnTypehint() + && !$this->checkPhpDocMissingReturn + && TypeCombinator::containsNull($returnType) + ) { return []; } if ($returnType instanceof NeverType && $returnType->isExplicit()) { + $errorBuilder = RuleErrorBuilder::message(sprintf('%s should always throw an exception or terminate script execution but doesn\'t do that.', $description))->line($node->getNode()->getStartLine()); + + if ($node->hasNativeReturnTypehint()) { + $errorBuilder->nonIgnorable(); + } + return [ - RuleErrorBuilder::message(sprintf('%s should always throw an exception or terminate script execution but doesn\'t do that.', $description))->line($node->getNode()->getStartLine())->build(), + $errorBuilder->build(), ]; } if ( $returnType instanceof MixedType && !$returnType instanceof TemplateMixedType + && !$node->hasNativeReturnTypehint() && ( !$returnType->isExplicitMixed() || !$this->checkExplicitMixedMissingReturn @@ -117,10 +126,16 @@ public function processNode(Node $node, Scope $scope): array return []; } + $errorBuilder = RuleErrorBuilder::message( + sprintf('%s should return %s but return statement is missing.', $description, $returnType->describe(VerbosityLevel::typeOnly())), + )->line($node->getNode()->getStartLine()); + + if ($node->hasNativeReturnTypehint()) { + $errorBuilder->nonIgnorable(); + } + return [ - RuleErrorBuilder::message( - sprintf('%s should return %s but return statement is missing.', $description, $returnType->describe(VerbosityLevel::typeOnly())) - )->line($node->getNode()->getStartLine())->build(), + $errorBuilder->build(), ]; } diff --git a/src/Rules/MissingTypehintCheck.php b/src/Rules/MissingTypehintCheck.php index 9d72badbc0..b360872811 100644 --- a/src/Rules/MissingTypehintCheck.php +++ b/src/Rules/MissingTypehintCheck.php @@ -2,7 +2,12 @@ namespace PHPStan\Rules; +use Closure; +use Generator; +use Iterator; +use IteratorAggregate; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryType; use PHPStan\Type\CallableType; use PHPStan\Type\Generic\GenericObjectType; @@ -14,6 +19,10 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeWithClassName; +use Traversable; +use function array_keys; +use function in_array; +use function sprintf; class MissingTypehintCheck { @@ -23,48 +32,27 @@ class MissingTypehintCheck public const TURN_OFF_NON_GENERIC_CHECK_TIP = 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.'; private const ITERABLE_GENERIC_CLASS_NAMES = [ - \Traversable::class, - \Iterator::class, - \IteratorAggregate::class, - \Generator::class, + Traversable::class, + Iterator::class, + IteratorAggregate::class, + Generator::class, ]; - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private bool $checkMissingIterableValueType; - - private bool $checkGenericClassInNonGenericObjectType; - - private bool $checkMissingCallableSignature; - - /** @var string[] */ - private array $skipCheckGenericClasses; - - private bool $deepInspectTypes; - /** * @param string[] $skipCheckGenericClasses */ public function __construct( - ReflectionProvider $reflectionProvider, - bool $checkMissingIterableValueType, - bool $checkGenericClassInNonGenericObjectType, - bool $checkMissingCallableSignature, - array $skipCheckGenericClasses = [], - bool $deepInspectTypes = false + private ReflectionProvider $reflectionProvider, + private bool $checkMissingIterableValueType, + private bool $checkGenericClassInNonGenericObjectType, + private bool $checkMissingCallableSignature, + private array $skipCheckGenericClasses, ) { - $this->reflectionProvider = $reflectionProvider; - $this->checkMissingIterableValueType = $checkMissingIterableValueType; - $this->checkGenericClassInNonGenericObjectType = $checkGenericClassInNonGenericObjectType; - $this->checkMissingCallableSignature = $checkMissingCallableSignature; - $this->skipCheckGenericClasses = $skipCheckGenericClasses; - $this->deepInspectTypes = $deepInspectTypes; } /** - * @param \PHPStan\Type\Type $type - * @return \PHPStan\Type\Type[] + * @return Type[] */ public function getIterableTypesWithMissingValueTypehint(Type $type): array { @@ -95,7 +83,7 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array } $iterablesWithMissingValueTypehint[] = $type; } - if ($this->deepInspectTypes && !$type instanceof IntersectionType) { + if (!$type instanceof IntersectionType) { return $traverse($type); } @@ -108,7 +96,6 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array } /** - * @param \PHPStan\Type\Type $type * @return array */ public function getNonGenericObjectTypesWithGenericClass(Type $type): array @@ -147,7 +134,7 @@ public function getNonGenericObjectTypesWithGenericClass(Type $type): array $resolvedType = TemplateTypeHelper::resolveToBounds($type); if (!$resolvedType instanceof ObjectType) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $objectTypes[] = [ sprintf('%s %s', $classReflection->isInterface() ? 'interface' : 'class', $classReflection->getDisplayName(false)), @@ -163,8 +150,7 @@ public function getNonGenericObjectTypesWithGenericClass(Type $type): array } /** - * @param \PHPStan\Type\Type $type - * @return \PHPStan\Type\Type[] + * @return Type[] */ public function getCallablesWithMissingSignature(Type $type): array { @@ -176,7 +162,7 @@ public function getCallablesWithMissingSignature(Type $type): array TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$result): Type { if ( ($type instanceof CallableType && $type->isCommonCallable()) || - ($type instanceof ObjectType && $type->getClassName() === \Closure::class)) { + ($type instanceof ObjectType && $type->getClassName() === Closure::class)) { $result[] = $type; } return $traverse($type); diff --git a/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php b/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php index b6854978f2..c4cd4231cf 100644 --- a/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php +++ b/src/Rules/Namespaces/ExistingNamesInGroupUseRule.php @@ -8,35 +8,31 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; +use function count; +use function sprintf; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\GroupUse> + * @implements Rule */ -class ExistingNamesInGroupUseRule implements \PHPStan\Rules\Rule +class ExistingNamesInGroupUseRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private bool $checkFunctionNameCase; - public function __construct( - ReflectionProvider $reflectionProvider, - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - bool $checkFunctionNameCase + private ReflectionProvider $reflectionProvider, + private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private bool $checkFunctionNameCase, ) { - $this->reflectionProvider = $reflectionProvider; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->checkFunctionNameCase = $checkFunctionNameCase; } public function getNodeType(): string { - return \PhpParser\Node\Stmt\GroupUse::class; + return Node\Stmt\GroupUse::class; } public function processNode(Node $node, Scope $scope): array @@ -60,7 +56,7 @@ public function processNode(Node $node, Scope $scope): array } elseif ($use->type === Use_::TYPE_NORMAL) { $error = $this->checkClass($name); } else { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ($error === null) { @@ -99,7 +95,7 @@ private function checkFunction(Node\Name $name): ?RuleError return RuleErrorBuilder::message(sprintf( 'Function %s used with incorrect case: %s.', $realName, - $usedName + $usedName, ))->line($name->getLine())->build(); } } @@ -118,7 +114,7 @@ private function checkClass(Node\Name $name): ?RuleError return $errors[0]; } - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } } diff --git a/src/Rules/Namespaces/ExistingNamesInUseRule.php b/src/Rules/Namespaces/ExistingNamesInUseRule.php index 40a575934b..3bb023ad25 100644 --- a/src/Rules/Namespaces/ExistingNamesInUseRule.php +++ b/src/Rules/Namespaces/ExistingNamesInUseRule.php @@ -7,46 +7,42 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; +use function array_map; +use function sprintf; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Use_> + * @implements Rule */ -class ExistingNamesInUseRule implements \PHPStan\Rules\Rule +class ExistingNamesInUseRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private bool $checkFunctionNameCase; - public function __construct( - ReflectionProvider $reflectionProvider, - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - bool $checkFunctionNameCase + private ReflectionProvider $reflectionProvider, + private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private bool $checkFunctionNameCase, ) { - $this->reflectionProvider = $reflectionProvider; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->checkFunctionNameCase = $checkFunctionNameCase; } public function getNodeType(): string { - return \PhpParser\Node\Stmt\Use_::class; + return Node\Stmt\Use_::class; } public function processNode(Node $node, Scope $scope): array { if ($node->type === Node\Stmt\Use_::TYPE_UNKNOWN) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } foreach ($node->uses as $use) { if ($use->type !== Node\Stmt\Use_::TYPE_UNKNOWN) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } } @@ -62,7 +58,7 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param \PhpParser\Node\Stmt\UseUse[] $uses + * @param Node\Stmt\UseUse[] $uses * @return RuleError[] */ private function checkConstants(array $uses): array @@ -80,7 +76,7 @@ private function checkConstants(array $uses): array } /** - * @param \PhpParser\Node\Stmt\UseUse[] $uses + * @param Node\Stmt\UseUse[] $uses * @return RuleError[] */ private function checkFunctions(array $uses): array @@ -100,7 +96,7 @@ private function checkFunctions(array $uses): array $errors[] = RuleErrorBuilder::message(sprintf( 'Function %s used with incorrect case: %s.', $realName, - $usedName + $usedName, ))->line($use->name->getLine())->build(); } } @@ -110,15 +106,13 @@ private function checkFunctions(array $uses): array } /** - * @param \PhpParser\Node\Stmt\UseUse[] $uses + * @param Node\Stmt\UseUse[] $uses * @return RuleError[] */ private function checkClasses(array $uses): array { return $this->classCaseSensitivityCheck->checkClassNames( - array_map(static function (\PhpParser\Node\Stmt\UseUse $use): ClassNameNodePair { - return new ClassNameNodePair((string) $use->name, $use->name); - }, $uses) + array_map(static fn (Node\Stmt\UseUse $use): ClassNameNodePair => new ClassNameNodePair((string) $use->name, $use->name), $uses), ); } diff --git a/src/Rules/Operators/InvalidAssignVarRule.php b/src/Rules/Operators/InvalidAssignVarRule.php index 2c700f4e81..7495c81e27 100644 --- a/src/Rules/Operators/InvalidAssignVarRule.php +++ b/src/Rules/Operators/InvalidAssignVarRule.php @@ -18,11 +18,8 @@ class InvalidAssignVarRule implements Rule { - private NullsafeCheck $nullsafeCheck; - - public function __construct(NullsafeCheck $nullsafeCheck) + public function __construct(private NullsafeCheck $nullsafeCheck) { - $this->nullsafeCheck = $nullsafeCheck; } public function getNodeType(): string diff --git a/src/Rules/Operators/InvalidBinaryOperationRule.php b/src/Rules/Operators/InvalidBinaryOperationRule.php index 9cabaec6be..f9faf4a8f0 100644 --- a/src/Rules/Operators/InvalidBinaryOperationRule.php +++ b/src/Rules/Operators/InvalidBinaryOperationRule.php @@ -3,31 +3,31 @@ namespace PHPStan\Rules\Operators; use PhpParser\Node; +use PhpParser\PrettyPrinter\Standard; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function sprintf; +use function strlen; +use function substr; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr> + * @implements Rule */ -class InvalidBinaryOperationRule implements \PHPStan\Rules\Rule +class InvalidBinaryOperationRule implements Rule { - private \PhpParser\PrettyPrinter\Standard $printer; - - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - public function __construct( - \PhpParser\PrettyPrinter\Standard $printer, - RuleLevelHelper $ruleLevelHelper + private Standard $printer, + private RuleLevelHelper $ruleLevelHelper, ) { - $this->printer = $printer; - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string @@ -35,7 +35,7 @@ public function getNodeType(): string return Node\Expr::class; } - public function processNode(\PhpParser\Node $node, Scope $scope): array + public function processNode(Node $node, Scope $scope): array { if ( !$node instanceof Node\Expr\BinaryOp @@ -64,20 +64,16 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array } if ($node instanceof Node\Expr\AssignOp\Concat || $node instanceof Node\Expr\BinaryOp\Concat) { - $callback = static function (Type $type): bool { - return !$type->toString() instanceof ErrorType; - }; + $callback = static fn (Type $type): bool => !$type->toString() instanceof ErrorType; } else { - $callback = static function (Type $type): bool { - return !$type->toNumber() instanceof ErrorType; - }; + $callback = static fn (Type $type): bool => !$type->toNumber() instanceof ErrorType; } $leftType = $this->ruleLevelHelper->findTypeToCheck( $scope, $left, '', - $callback + $callback, )->getType(); if ($leftType instanceof ErrorType) { return []; @@ -87,14 +83,14 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array $scope, $right, '', - $callback + $callback, )->getType(); if ($rightType instanceof ErrorType) { return []; } if (!$scope instanceof MutatingScope) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $scope = $scope @@ -110,7 +106,7 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array 'Binary operation "%s" between %s and %s results in an error.', substr(substr($this->printer->prettyPrintExpr($newNode), strlen($leftName) + 2), 0, -(strlen($rightName) + 2)), $scope->getType($left)->describe(VerbosityLevel::value()), - $scope->getType($right)->describe(VerbosityLevel::value()) + $scope->getType($right)->describe(VerbosityLevel::value()), ))->line($left->getLine())->build(), ]; } diff --git a/src/Rules/Operators/InvalidComparisonOperationRule.php b/src/Rules/Operators/InvalidComparisonOperationRule.php index 83c4ac30ad..24110f32dd 100644 --- a/src/Rules/Operators/InvalidComparisonOperationRule.php +++ b/src/Rules/Operators/InvalidComparisonOperationRule.php @@ -4,27 +4,29 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\ErrorType; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; +use PHPStan\Type\NullType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\BinaryOp> + * @implements Rule */ -class InvalidComparisonOperationRule implements \PHPStan\Rules\Rule +class InvalidComparisonOperationRule implements Rule { - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - public function __construct(RuleLevelHelper $ruleLevelHelper) + public function __construct(private RuleLevelHelper $ruleLevelHelper) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string @@ -48,10 +50,10 @@ public function processNode(Node $node, Scope $scope): array if ( ($this->isNumberType($scope, $node->left) && ( - $this->isObjectType($scope, $node->right) || $this->isArrayType($scope, $node->right) + $this->isPossiblyNullableObjectType($scope, $node->right) || $this->isPossiblyNullableArrayType($scope, $node->right) )) || ($this->isNumberType($scope, $node->right) && ( - $this->isObjectType($scope, $node->left) || $this->isArrayType($scope, $node->left) + $this->isPossiblyNullableObjectType($scope, $node->left) || $this->isPossiblyNullableArrayType($scope, $node->left) )) ) { return [ @@ -59,7 +61,7 @@ public function processNode(Node $node, Scope $scope): array 'Comparison operation "%s" between %s and %s results in an error.', $node->getOperatorSigil(), $scope->getType($node->left)->describe(VerbosityLevel::value()), - $scope->getType($node->right)->describe(VerbosityLevel::value()) + $scope->getType($node->right)->describe(VerbosityLevel::value()), ))->line($node->left->getLine())->build(), ]; } @@ -70,9 +72,7 @@ public function processNode(Node $node, Scope $scope): array private function isNumberType(Scope $scope, Node\Expr $expr): bool { $acceptedType = new UnionType([new IntegerType(), new FloatType()]); - $onlyNumber = static function (Type $type) use ($acceptedType): bool { - return $acceptedType->accepts($type, true)->yes(); - }; + $onlyNumber = static fn (Type $type): bool => $acceptedType->accepts($type, true)->yes(); $type = $this->ruleLevelHelper->findTypeToCheck($scope, $expr, '', $onlyNumber)->getType(); @@ -86,7 +86,7 @@ private function isNumberType(Scope $scope, Node\Expr $expr): bool return !$acceptedType->isSuperTypeOf($type)->no(); } - private function isObjectType(Scope $scope, Node\Expr $expr): bool + private function isPossiblyNullableObjectType(Scope $scope, Node\Expr $expr): bool { $acceptedType = new ObjectWithoutClassType(); @@ -94,34 +94,38 @@ private function isObjectType(Scope $scope, Node\Expr $expr): bool $scope, $expr, '', - static function (Type $type) use ($acceptedType): bool { - return $acceptedType->isSuperTypeOf($type)->yes(); - } + static fn (Type $type): bool => $acceptedType->isSuperTypeOf($type)->yes(), )->getType(); if ($type instanceof ErrorType) { return false; } + if (TypeCombinator::containsNull($type) && !$type instanceof NullType) { + $type = TypeCombinator::removeNull($type); + } + $isSuperType = $acceptedType->isSuperTypeOf($type); - if ($type instanceof \PHPStan\Type\BenevolentUnionType) { + if ($type instanceof BenevolentUnionType) { return !$isSuperType->no(); } return $isSuperType->yes(); } - private function isArrayType(Scope $scope, Node\Expr $expr): bool + private function isPossiblyNullableArrayType(Scope $scope, Node\Expr $expr): bool { $type = $this->ruleLevelHelper->findTypeToCheck( $scope, $expr, '', - static function (Type $type): bool { - return $type->isArray()->yes(); - } + static fn (Type $type): bool => $type->isArray()->yes(), )->getType(); + if (TypeCombinator::containsNull($type) && !$type instanceof NullType) { + $type = TypeCombinator::removeNull($type); + } + return !($type instanceof ErrorType) && $type->isArray()->yes(); } diff --git a/src/Rules/Operators/InvalidIncDecOperationRule.php b/src/Rules/Operators/InvalidIncDecOperationRule.php index 1bbc260fa5..1bd9d46d0e 100644 --- a/src/Rules/Operators/InvalidIncDecOperationRule.php +++ b/src/Rules/Operators/InvalidIncDecOperationRule.php @@ -2,51 +2,52 @@ namespace PHPStan\Rules\Operators; +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ErrorType; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr> + * @implements Rule */ -class InvalidIncDecOperationRule implements \PHPStan\Rules\Rule +class InvalidIncDecOperationRule implements Rule { - private bool $checkThisOnly; - - public function __construct(bool $checkThisOnly) + public function __construct(private bool $checkThisOnly) { - $this->checkThisOnly = $checkThisOnly; } public function getNodeType(): string { - return \PhpParser\Node\Expr::class; + return Node\Expr::class; } - public function processNode(\PhpParser\Node $node, \PHPStan\Analyser\Scope $scope): array + public function processNode(Node $node, Scope $scope): array { if ( - !$node instanceof \PhpParser\Node\Expr\PreInc - && !$node instanceof \PhpParser\Node\Expr\PostInc - && !$node instanceof \PhpParser\Node\Expr\PreDec - && !$node instanceof \PhpParser\Node\Expr\PostDec + !$node instanceof Node\Expr\PreInc + && !$node instanceof Node\Expr\PostInc + && !$node instanceof Node\Expr\PreDec + && !$node instanceof Node\Expr\PostDec ) { return []; } - $operatorString = $node instanceof \PhpParser\Node\Expr\PreInc || $node instanceof \PhpParser\Node\Expr\PostInc ? '++' : '--'; + $operatorString = $node instanceof Node\Expr\PreInc || $node instanceof Node\Expr\PostInc ? '++' : '--'; if ( - !$node->var instanceof \PhpParser\Node\Expr\Variable - && !$node->var instanceof \PhpParser\Node\Expr\ArrayDimFetch - && !$node->var instanceof \PhpParser\Node\Expr\PropertyFetch - && !$node->var instanceof \PhpParser\Node\Expr\StaticPropertyFetch + !$node->var instanceof Node\Expr\Variable + && !$node->var instanceof Node\Expr\ArrayDimFetch + && !$node->var instanceof Node\Expr\PropertyFetch + && !$node->var instanceof Node\Expr\StaticPropertyFetch ) { return [ RuleErrorBuilder::message(sprintf( 'Cannot use %s on a non-variable.', - $operatorString + $operatorString, ))->line($node->var->getLine())->build(), ]; } @@ -64,7 +65,7 @@ public function processNode(\PhpParser\Node $node, \PHPStan\Analyser\Scope $scop RuleErrorBuilder::message(sprintf( 'Cannot use %s on %s.', $operatorString, - $varType->describe(VerbosityLevel::value()) + $varType->describe(VerbosityLevel::value()), ))->line($node->var->getLine())->build(), ]; } diff --git a/src/Rules/Operators/InvalidUnaryOperationRule.php b/src/Rules/Operators/InvalidUnaryOperationRule.php index feddcd33be..7c3ac12ecf 100644 --- a/src/Rules/Operators/InvalidUnaryOperationRule.php +++ b/src/Rules/Operators/InvalidUnaryOperationRule.php @@ -2,37 +2,40 @@ namespace PHPStan\Rules\Operators; +use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ErrorType; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr> + * @implements Rule */ -class InvalidUnaryOperationRule implements \PHPStan\Rules\Rule +class InvalidUnaryOperationRule implements Rule { public function getNodeType(): string { - return \PhpParser\Node\Expr::class; + return Node\Expr::class; } - public function processNode(\PhpParser\Node $node, Scope $scope): array + public function processNode(Node $node, Scope $scope): array { if ( - !$node instanceof \PhpParser\Node\Expr\UnaryPlus - && !$node instanceof \PhpParser\Node\Expr\UnaryMinus - && !$node instanceof \PhpParser\Node\Expr\BitwiseNot + !$node instanceof Node\Expr\UnaryPlus + && !$node instanceof Node\Expr\UnaryMinus + && !$node instanceof Node\Expr\BitwiseNot ) { return []; } if ($scope->getType($node) instanceof ErrorType) { - if ($node instanceof \PhpParser\Node\Expr\UnaryPlus) { + if ($node instanceof Node\Expr\UnaryPlus) { $operator = '+'; - } elseif ($node instanceof \PhpParser\Node\Expr\UnaryMinus) { + } elseif ($node instanceof Node\Expr\UnaryMinus) { $operator = '-'; } else { $operator = '~'; @@ -41,7 +44,7 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array RuleErrorBuilder::message(sprintf( 'Unary operation "%s" on %s results in an error.', $operator, - $scope->getType($node->expr)->describe(VerbosityLevel::value()) + $scope->getType($node->expr)->describe(VerbosityLevel::value()), ))->line($node->expr->getLine())->build(), ]; } diff --git a/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php b/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php new file mode 100644 index 0000000000..ace957a7ba --- /dev/null +++ b/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php @@ -0,0 +1,130 @@ + + */ +class IncompatibleClassConstantPhpDocTypeRule implements Rule +{ + + public function __construct( + private GenericObjectTypeCheck $genericObjectTypeCheck, + private UnresolvableTypeHelper $unresolvableTypeHelper, + ) + { + } + + public function getNodeType(): string + { + return Node\Stmt\ClassConst::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } + + $errors = []; + foreach ($node->consts as $const) { + $constantName = $const->name->toString(); + $errors = array_merge($errors, $this->processSingleConstant($scope->getClassReflection(), $constantName)); + } + + return $errors; + } + + /** + * @return RuleError[] + */ + private function processSingleConstant(ClassReflection $classReflection, string $constantName): array + { + $constantReflection = $classReflection->getConstant($constantName); + if (!$constantReflection instanceof ClassConstantReflection) { + return []; + } + + if (!$constantReflection->hasPhpDocType()) { + return []; + } + + $phpDocType = $constantReflection->getValueType(); + + $errors = []; + if ( + $this->unresolvableTypeHelper->containsUnresolvableType($phpDocType) + ) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'PHPDoc tag @var for constant %s::%s contains unresolvable type.', + $constantReflection->getDeclaringClass()->getName(), + $constantName, + ))->build(); + } else { + $nativeType = ConstantTypeHelper::getTypeFromValue($constantReflection->getValue()); + $isSuperType = $phpDocType->isSuperTypeOf($nativeType); + $verbosity = VerbosityLevel::getRecommendedLevelByType($phpDocType, $nativeType); + if ($isSuperType->no()) { + $errors[] = 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), + $nativeType->describe(VerbosityLevel::value()), + ))->build(); + + } elseif ($isSuperType->maybe()) { + $errors[] = 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), + $nativeType->describe(VerbosityLevel::value()), + ))->build(); + } + } + + $className = SprintfHelper::escapeFormatString($constantReflection->getDeclaringClass()->getDisplayName()); + $escapedConstantName = SprintfHelper::escapeFormatString($constantName); + + return array_merge($errors, $this->genericObjectTypeCheck->check( + $phpDocType, + sprintf( + 'PHPDoc tag @var for constant %s::%s contains generic type %%s but %%s %%s is not generic.', + $className, + $escapedConstantName, + ), + sprintf( + 'Generic type %%s in PHPDoc tag @var for constant %s::%s does not specify all template types of %%s %%s: %%s', + $className, + $escapedConstantName, + ), + sprintf( + 'Generic type %%s in PHPDoc tag @var for constant %s::%s specifies %%d template types, but %%s %%s supports only %%d: %%s', + $className, + $escapedConstantName, + ), + sprintf( + 'Type %%s in generic type %%s in PHPDoc tag @var for constant %s::%s is not subtype of template type %%s of %%s %%s.', + $className, + $escapedConstantName, + ), + )); + } + +} diff --git a/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php b/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php index 6a05e6131b..8c8a870b65 100644 --- a/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php +++ b/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php @@ -5,40 +5,38 @@ use PhpParser\Node; use PhpParser\Node\Expr\Variable; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Rules\Generics\GenericObjectTypeCheck; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\ArrayType; use PHPStan\Type\FileTypeMapper; -use PHPStan\Type\Generic\TemplateTypeHelper; +use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function array_merge; +use function is_string; +use function sprintf; +use function trim; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\FunctionLike> + * @implements Rule */ -class IncompatiblePhpDocTypeRule implements \PHPStan\Rules\Rule +class IncompatiblePhpDocTypeRule implements Rule { - private FileTypeMapper $fileTypeMapper; - - private \PHPStan\Rules\Generics\GenericObjectTypeCheck $genericObjectTypeCheck; - - private UnresolvableTypeHelper $unresolvableTypeHelper; - public function __construct( - FileTypeMapper $fileTypeMapper, - GenericObjectTypeCheck $genericObjectTypeCheck, - UnresolvableTypeHelper $unresolvableTypeHelper + private FileTypeMapper $fileTypeMapper, + private GenericObjectTypeCheck $genericObjectTypeCheck, + private UnresolvableTypeHelper $unresolvableTypeHelper, ) { - $this->fileTypeMapper = $fileTypeMapper; - $this->genericObjectTypeCheck = $genericObjectTypeCheck; - $this->unresolvableTypeHelper = $unresolvableTypeHelper; } public function getNodeType(): string { - return \PhpParser\Node\FunctionLike::class; + return Node\FunctionLike::class; } public function processNode(Node $node, Scope $scope): array @@ -62,7 +60,7 @@ public function processNode(Node $node, Scope $scope): array $scope->isInClass() ? $scope->getClassReflection()->getName() : null, $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, $functionName, - $docComment->getText() + $docComment->getText(), ); $nativeParameterTypes = $this->getNativeParameterTypes($node, $scope); $nativeReturnType = $this->getNativeReturnType($node, $scope); @@ -74,7 +72,7 @@ public function processNode(Node $node, Scope $scope): array if (!isset($nativeParameterTypes[$parameterName])) { $errors[] = RuleErrorBuilder::message(sprintf( 'PHPDoc tag @param references unknown parameter: $%s', - $parameterName + $parameterName, ))->identifier('phpDoc.unknownParameter')->metadata(['parameterName' => $parameterName])->build(); } elseif ( @@ -82,7 +80,7 @@ public function processNode(Node $node, Scope $scope): array ) { $errors[] = RuleErrorBuilder::message(sprintf( 'PHPDoc tag @param for parameter $%s contains unresolvable type.', - $parameterName + $parameterName, ))->build(); } else { @@ -94,26 +92,28 @@ public function processNode(Node $node, Scope $scope): array ) { $phpDocParamType = $phpDocParamType->getItemType(); } - $isParamSuperType = $nativeParamType->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocParamType)); + $isParamSuperType = $nativeParamType->isSuperTypeOf($phpDocParamType); + + $escapedParameterName = SprintfHelper::escapeFormatString($parameterName); $errors = array_merge($errors, $this->genericObjectTypeCheck->check( $phpDocParamType, sprintf( - 'PHPDoc tag @param for parameter $%s contains generic type %%s but class %%s is not generic.', - $parameterName + 'PHPDoc tag @param for parameter $%s contains generic type %%s but %%s %%s is not generic.', + $escapedParameterName, ), sprintf( - 'Generic type %%s in PHPDoc tag @param for parameter $%s does not specify all template types of class %%s: %%s', - $parameterName + 'Generic type %%s in PHPDoc tag @param for parameter $%s does not specify all template types of %%s %%s: %%s', + $escapedParameterName, ), sprintf( - 'Generic type %%s in PHPDoc tag @param for parameter $%s specifies %%d template types, but class %%s supports only %%d: %%s', - $parameterName + 'Generic type %%s in PHPDoc tag @param for parameter $%s specifies %%d template types, but %%s %%s supports only %%d: %%s', + $escapedParameterName, ), sprintf( - 'Type %%s in generic type %%s in PHPDoc tag @param for parameter $%s is not subtype of template type %%s of class %%s.', - $parameterName - ) + 'Type %%s in generic type %%s in PHPDoc tag @param for parameter $%s is not subtype of template type %%s of %%s %%s.', + $escapedParameterName, + ), )); if ($isParamSuperType->no()) { @@ -121,22 +121,27 @@ public function processNode(Node $node, Scope $scope): array 'PHPDoc tag @param for parameter $%s with type %s is incompatible with native type %s.', $parameterName, $phpDocParamType->describe(VerbosityLevel::typeOnly()), - $nativeParamType->describe(VerbosityLevel::typeOnly()) + $nativeParamType->describe(VerbosityLevel::typeOnly()), ))->build(); } elseif ($isParamSuperType->maybe()) { - $errors[] = RuleErrorBuilder::message(sprintf( + $errorBuilder = RuleErrorBuilder::message(sprintf( 'PHPDoc tag @param for parameter $%s with type %s is not subtype of native type %s.', $parameterName, $phpDocParamType->describe(VerbosityLevel::typeOnly()), - $nativeParamType->describe(VerbosityLevel::typeOnly()) - ))->build(); + $nativeParamType->describe(VerbosityLevel::typeOnly()), + )); + if ($phpDocParamType instanceof TemplateType) { + $errorBuilder->tip(sprintf('Write @template %s of %s to fix this.', $phpDocParamType->getName(), $nativeParamType->describe(VerbosityLevel::typeOnly()))); + } + + $errors[] = $errorBuilder->build(); } } } if ($resolvedPhpDoc->getReturnTag() !== null) { - $phpDocReturnType = TemplateTypeHelper::resolveToBounds($resolvedPhpDoc->getReturnTag()->getType()); + $phpDocReturnType = $resolvedPhpDoc->getReturnTag()->getType(); if ( $this->unresolvableTypeHelper->containsUnresolvableType($phpDocReturnType) @@ -147,24 +152,29 @@ public function processNode(Node $node, Scope $scope): array $isReturnSuperType = $nativeReturnType->isSuperTypeOf($phpDocReturnType); $errors = array_merge($errors, $this->genericObjectTypeCheck->check( $phpDocReturnType, - 'PHPDoc tag @return contains generic type %s but class %s is not generic.', - 'Generic type %s in PHPDoc tag @return does not specify all template types of class %s: %s', - 'Generic type %s in PHPDoc tag @return specifies %d template types, but class %s supports only %d: %s', - 'Type %s in generic type %s in PHPDoc tag @return is not subtype of template type %s of class %s.' + 'PHPDoc tag @return contains generic type %s but %s %s is not generic.', + '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.', )); if ($isReturnSuperType->no()) { $errors[] = RuleErrorBuilder::message(sprintf( 'PHPDoc tag @return with type %s is incompatible with native type %s.', $phpDocReturnType->describe(VerbosityLevel::typeOnly()), - $nativeReturnType->describe(VerbosityLevel::typeOnly()) + $nativeReturnType->describe(VerbosityLevel::typeOnly()), ))->build(); } elseif ($isReturnSuperType->maybe()) { - $errors[] = RuleErrorBuilder::message(sprintf( + $errorBuilder = RuleErrorBuilder::message(sprintf( 'PHPDoc tag @return with type %s is not subtype of native type %s.', $phpDocReturnType->describe(VerbosityLevel::typeOnly()), - $nativeReturnType->describe(VerbosityLevel::typeOnly()) - ))->build(); + $nativeReturnType->describe(VerbosityLevel::typeOnly()), + )); + if ($phpDocReturnType instanceof TemplateType) { + $errorBuilder->tip(sprintf('Write @template %s of %s to fix this.', $phpDocReturnType->getName(), $nativeReturnType->describe(VerbosityLevel::typeOnly()))); + } + + $errors[] = $errorBuilder->build(); } } } @@ -173,29 +183,27 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param Node\FunctionLike $node - * @param Scope $scope * @return Type[] */ - private function getNativeParameterTypes(\PhpParser\Node\FunctionLike $node, Scope $scope): array + private function getNativeParameterTypes(Node\FunctionLike $node, Scope $scope): array { $nativeParameterTypes = []; foreach ($node->getParams() as $parameter) { $isNullable = $scope->isParameterValueNullable($parameter); if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $nativeParameterTypes[$parameter->var->name] = $scope->getFunctionType( $parameter->type, $isNullable, - false + false, ); } return $nativeParameterTypes; } - private function getNativeReturnType(\PhpParser\Node\FunctionLike $node, Scope $scope): Type + private function getNativeReturnType(Node\FunctionLike $node, Scope $scope): Type { return $scope->getFunctionType($node->getReturnType(), false, false); } diff --git a/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php b/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php index 74b813e37e..bc053796de 100644 --- a/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php +++ b/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php @@ -4,29 +4,28 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Node\ClassPropertyNode; use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; +use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\VerbosityLevel; +use function array_merge; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\ClassPropertyNode> + * @implements Rule */ class IncompatiblePropertyPhpDocTypeRule implements Rule { - private \PHPStan\Rules\Generics\GenericObjectTypeCheck $genericObjectTypeCheck; - - private UnresolvableTypeHelper $unresolvableTypeHelper; - public function __construct( - GenericObjectTypeCheck $genericObjectTypeCheck, - UnresolvableTypeHelper $unresolvableTypeHelper + private GenericObjectTypeCheck $genericObjectTypeCheck, + private UnresolvableTypeHelper $unresolvableTypeHelper, ) { - $this->genericObjectTypeCheck = $genericObjectTypeCheck; - $this->unresolvableTypeHelper = $unresolvableTypeHelper; } public function getNodeType(): string @@ -37,13 +36,13 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $propertyName = $node->getName(); $propertyReflection = $scope->getClassReflection()->getNativeProperty($propertyName); - if (!$propertyReflection->hasPhpDoc()) { + if (!$propertyReflection->hasPhpDocType()) { return []; } @@ -61,7 +60,7 @@ public function processNode(Node $node, Scope $scope): array '%s for property %s::$%s contains unresolvable type.', $description, $propertyReflection->getDeclaringClass()->getName(), - $propertyName + $propertyName, ))->build(); } @@ -74,46 +73,55 @@ public function processNode(Node $node, Scope $scope): array $propertyReflection->getDeclaringClass()->getDisplayName(), $propertyName, $phpDocType->describe(VerbosityLevel::typeOnly()), - $nativeType->describe(VerbosityLevel::typeOnly()) + $nativeType->describe(VerbosityLevel::typeOnly()), ))->build(); } elseif ($isSuperType->maybe()) { - $messages[] = RuleErrorBuilder::message(sprintf( + $errorBuilder = RuleErrorBuilder::message(sprintf( '%s for property %s::$%s with type %s is not subtype of native type %s.', $description, $propertyReflection->getDeclaringClass()->getDisplayName(), $propertyName, $phpDocType->describe(VerbosityLevel::typeOnly()), - $nativeType->describe(VerbosityLevel::typeOnly()) - ))->build(); + $nativeType->describe(VerbosityLevel::typeOnly()), + )); + + if ($phpDocType instanceof TemplateType) { + $errorBuilder->tip(sprintf('Write @template %s of %s to fix this.', $phpDocType->getName(), $nativeType->describe(VerbosityLevel::typeOnly()))); + } + + $messages[] = $errorBuilder->build(); } + $className = SprintfHelper::escapeFormatString($propertyReflection->getDeclaringClass()->getDisplayName()); + $escapedPropertyName = SprintfHelper::escapeFormatString($propertyName); + $messages = array_merge($messages, $this->genericObjectTypeCheck->check( $phpDocType, sprintf( - '%s for property %s::$%s contains generic type %%s but class %%s is not generic.', + '%s for property %s::$%s contains generic type %%s but %%s %%s is not generic.', $description, - $propertyReflection->getDeclaringClass()->getDisplayName(), - $propertyName + $className, + $escapedPropertyName, ), sprintf( - 'Generic type %%s in %s for property %s::$%s does not specify all template types of class %%s: %%s', + 'Generic type %%s in %s for property %s::$%s does not specify all template types of %%s %%s: %%s', $description, - $propertyReflection->getDeclaringClass()->getDisplayName(), - $propertyName + $className, + $escapedPropertyName, ), sprintf( - 'Generic type %%s in %s for property %s::$%s specifies %%d template types, but class %%s supports only %%d: %%s', + 'Generic type %%s in %s for property %s::$%s specifies %%d template types, but %%s %%s supports only %%d: %%s', $description, - $propertyReflection->getDeclaringClass()->getDisplayName(), - $propertyName + $className, + $escapedPropertyName, ), sprintf( - 'Type %%s in generic type %%s in %s for property %s::$%s is not subtype of template type %%s of class %%s.', + 'Type %%s in generic type %%s in %s for property %s::$%s is not subtype of template type %%s of %%s %%s.', $description, - $propertyReflection->getDeclaringClass()->getDisplayName(), - $propertyName - ) + $className, + $escapedPropertyName, + ), )); return $messages; diff --git a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php index f17d2df84e..2b3d22b15c 100644 --- a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php +++ b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php @@ -7,12 +7,16 @@ 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 in_array; +use function sprintf; +use function strpos; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node> + * @implements Rule */ -class InvalidPHPStanDocTagRule implements \PHPStan\Rules\Rule +class InvalidPHPStanDocTagRule implements Rule { private const POSSIBLE_PHPSTAN_TAGS = [ @@ -33,21 +37,18 @@ class InvalidPHPStanDocTagRule implements \PHPStan\Rules\Rule '@phpstan-impure', '@phpstan-type', '@phpstan-import-type', + '@phpstan-property', + '@phpstan-property-read', + '@phpstan-property-write', ]; - private Lexer $phpDocLexer; - - private PhpDocParser $phpDocParser; - - public function __construct(Lexer $phpDocLexer, PhpDocParser $phpDocParser) + public function __construct(private Lexer $phpDocLexer, private PhpDocParser $phpDocParser) { - $this->phpDocLexer = $phpDocLexer; - $this->phpDocParser = $phpDocParser; } public function getNodeType(): string { - return \PhpParser\Node::class; + return Node::class; } public function processNode(Node $node, Scope $scope): array @@ -81,7 +82,7 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( 'Unknown PHPDoc tag: %s', - $phpDocTag->name + $phpDocTag->name, ))->build(); } diff --git a/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php b/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php index 3b0260a001..bcf90bec7e 100644 --- a/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php +++ b/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php @@ -8,27 +8,24 @@ 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; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node> + * @implements Rule */ -class InvalidPhpDocTagValueRule implements \PHPStan\Rules\Rule +class InvalidPhpDocTagValueRule implements Rule { - private Lexer $phpDocLexer; - - private PhpDocParser $phpDocParser; - - public function __construct(Lexer $phpDocLexer, PhpDocParser $phpDocParser) + public function __construct(private Lexer $phpDocLexer, private PhpDocParser $phpDocParser) { - $this->phpDocLexer = $phpDocLexer; - $this->phpDocParser = $phpDocParser; } public function getNodeType(): string { - return \PhpParser\Node::class; + return Node::class; } public function processNode(Node $node, Scope $scope): array @@ -40,6 +37,7 @@ public function processNode(Node $node, Scope $scope): array && !$node instanceof Node\Stmt\Property && !$node instanceof Node\Expr\Assign && !$node instanceof Node\Expr\AssignRef + && !$node instanceof Node\Stmt\ClassConst ) { return []; } @@ -67,7 +65,7 @@ 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() + $phpDocTag->value->exception->getMessage(), ))->build(); } diff --git a/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php b/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php index eb81251a0f..ea97fa34ef 100644 --- a/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php +++ b/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassNameNodePair; @@ -13,54 +14,34 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\VerbosityLevel; +use function array_map; +use function array_merge; +use function implode; +use function is_string; use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt> + * @implements Rule */ class InvalidPhpDocVarTagTypeRule implements Rule { - private FileTypeMapper $fileTypeMapper; - - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private \PHPStan\Rules\Generics\GenericObjectTypeCheck $genericObjectTypeCheck; - - private MissingTypehintCheck $missingTypehintCheck; - - private UnresolvableTypeHelper $unresolvableTypeHelper; - - private bool $checkClassCaseSensitivity; - - private bool $checkMissingVarTagTypehint; - public function __construct( - FileTypeMapper $fileTypeMapper, - ReflectionProvider $reflectionProvider, - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - GenericObjectTypeCheck $genericObjectTypeCheck, - MissingTypehintCheck $missingTypehintCheck, - UnresolvableTypeHelper $unresolvableTypeHelper, - bool $checkClassCaseSensitivity, - bool $checkMissingVarTagTypehint + private FileTypeMapper $fileTypeMapper, + private ReflectionProvider $reflectionProvider, + private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private GenericObjectTypeCheck $genericObjectTypeCheck, + private MissingTypehintCheck $missingTypehintCheck, + private UnresolvableTypeHelper $unresolvableTypeHelper, + private bool $checkClassCaseSensitivity, + private bool $checkMissingVarTagTypehint, ) { - $this->fileTypeMapper = $fileTypeMapper; - $this->reflectionProvider = $reflectionProvider; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->genericObjectTypeCheck = $genericObjectTypeCheck; - $this->missingTypehintCheck = $missingTypehintCheck; - $this->unresolvableTypeHelper = $unresolvableTypeHelper; - $this->checkClassCaseSensitivity = $checkClassCaseSensitivity; - $this->checkMissingVarTagTypehint = $checkMissingVarTagTypehint; } public function getNodeType(): string { - return \PhpParser\Node\Stmt::class; + return Node\Stmt::class; } public function processNode(Node $node, Scope $scope): array @@ -68,6 +49,8 @@ public function processNode(Node $node, Scope $scope): array if ( $node instanceof Node\Stmt\Property || $node instanceof Node\Stmt\PropertyProperty + || $node instanceof Node\Stmt\ClassConst + || $node instanceof Node\Stmt\Const_ ) { return []; } @@ -83,7 +66,7 @@ public function processNode(Node $node, Scope $scope): array $scope->isInClass() ? $scope->getClassReflection()->getName() : null, $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, $function !== null ? $function->getName() : null, - $docComment->getText() + $docComment->getText(), ); $errors = []; @@ -106,17 +89,18 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( '%s has no value type specified in iterable type %s.', $identifier, - $iterableTypeDescription + $iterableTypeDescription, ))->tip(MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP)->build(); } } + $escapedIdentifier = SprintfHelper::escapeFormatString($identifier); $errors = array_merge($errors, $this->genericObjectTypeCheck->check( $varTagType, - sprintf('%s contains generic type %%s but class %%s is not generic.', $identifier), - sprintf('Generic type %%s in %s does not specify all template types of class %%s: %%s', $identifier), - sprintf('Generic type %%s in %s specifies %%d template types, but class %%s supports only %%d: %%s', $identifier), - sprintf('Type %%s in generic type %%s in %s is not subtype of template type %%s of class %%s.', $identifier) + sprintf('%s contains generic type %%s but %%s %%s is not generic.', $escapedIdentifier), + 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), )); foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($varTagType) as [$innerName, $genericTypeNames]) { @@ -124,7 +108,7 @@ public function processNode(Node $node, Scope $scope): array '%s contains generic %s but does not specify its types: %s', $identifier, $innerName, - implode(', ', $genericTypeNames) + implode(', ', $genericTypeNames), ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); } @@ -134,7 +118,7 @@ public function processNode(Node $node, Scope $scope): array if ($this->reflectionProvider->getClass($referencedClass)->isTrait()) { $errors[] = RuleErrorBuilder::message(sprintf( sprintf('%s has invalid type %%s.', $identifier), - $referencedClass + $referencedClass, ))->build(); } continue; @@ -142,7 +126,7 @@ public function processNode(Node $node, Scope $scope): array $errors[] = RuleErrorBuilder::message(sprintf( sprintf('%s contains unknown class %%s.', $identifier), - $referencedClass + $referencedClass, ))->discoveringSymbolsTip()->build(); } @@ -152,9 +136,7 @@ public function processNode(Node $node, Scope $scope): array $errors = array_merge( $errors, - $this->classCaseSensitivityCheck->checkClassNames(array_map(static function (string $class) use ($node): ClassNameNodePair { - return new ClassNameNodePair($class, $node); - }, $referencedClasses)) + $this->classCaseSensitivityCheck->checkClassNames(array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $node), $referencedClasses)), ); } diff --git a/src/Rules/PhpDoc/InvalidThrowsPhpDocValueRule.php b/src/Rules/PhpDoc/InvalidThrowsPhpDocValueRule.php index 8027a757f0..f3d40be686 100644 --- a/src/Rules/PhpDoc/InvalidThrowsPhpDocValueRule.php +++ b/src/Rules/PhpDoc/InvalidThrowsPhpDocValueRule.php @@ -4,28 +4,28 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\ObjectType; use PHPStan\Type\VerbosityLevel; use PHPStan\Type\VoidType; +use Throwable; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt> + * @implements Rule */ -class InvalidThrowsPhpDocValueRule implements \PHPStan\Rules\Rule +class InvalidThrowsPhpDocValueRule implements Rule { - private FileTypeMapper $fileTypeMapper; - - public function __construct(FileTypeMapper $fileTypeMapper) + public function __construct(private FileTypeMapper $fileTypeMapper) { - $this->fileTypeMapper = $fileTypeMapper; } public function getNodeType(): string { - return \PhpParser\Node\Stmt::class; + return Node\Stmt::class; } public function processNode(Node $node, Scope $scope): array @@ -49,7 +49,7 @@ public function processNode(Node $node, Scope $scope): array $scope->isInClass() ? $scope->getClassReflection()->getName() : null, $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, $functionName, - $docComment->getText() + $docComment->getText(), ); if ($resolvedPhpDoc->getThrowsTag() === null) { @@ -61,7 +61,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $isThrowsSuperType = (new ObjectType(\Throwable::class))->isSuperTypeOf($phpDocThrowsType); + $isThrowsSuperType = (new ObjectType(Throwable::class))->isSuperTypeOf($phpDocThrowsType); if ($isThrowsSuperType->yes()) { return []; } @@ -69,7 +69,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message(sprintf( 'PHPDoc tag @throws with type %s is not subtype of Throwable', - $phpDocThrowsType->describe(VerbosityLevel::typeOnly()) + $phpDocThrowsType->describe(VerbosityLevel::typeOnly()), ))->build(), ]; } diff --git a/src/Rules/PhpDoc/UnresolvableTypeHelper.php b/src/Rules/PhpDoc/UnresolvableTypeHelper.php index 36c58b7dbd..8b53af21d5 100644 --- a/src/Rules/PhpDoc/UnresolvableTypeHelper.php +++ b/src/Rules/PhpDoc/UnresolvableTypeHelper.php @@ -10,38 +10,23 @@ class UnresolvableTypeHelper { - private bool $deepInspectTypes; - - public function __construct(bool $deepInspectTypes) - { - $this->deepInspectTypes = $deepInspectTypes; - } - public function containsUnresolvableType(Type $type): bool { - if ($this->deepInspectTypes) { - $containsUnresolvable = false; - TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$containsUnresolvable): Type { - if ($type instanceof ErrorType) { - $containsUnresolvable = true; - return $type; - } - if ($type instanceof NeverType && !$type->isExplicit()) { - $containsUnresolvable = true; - return $type; - } - - return $traverse($type); - }); - - return $containsUnresolvable; - } - - if ($type instanceof ErrorType) { - return true; - } - - return $type instanceof NeverType && !$type->isExplicit(); + $containsUnresolvable = false; + TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$containsUnresolvable): Type { + if ($type instanceof ErrorType) { + $containsUnresolvable = true; + return $type; + } + if ($type instanceof NeverType && !$type->isExplicit()) { + $containsUnresolvable = true; + return $type; + } + + return $traverse($type); + }); + + return $containsUnresolvable; } } diff --git a/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php b/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php index 18c95f0bc4..c1f3dac121 100644 --- a/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php +++ b/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php @@ -10,32 +10,37 @@ use PHPStan\Node\InClassNode; use PHPStan\Node\InFunctionNode; use PHPStan\Node\VirtualNode; +use PHPStan\PhpDoc\Tag\VarTag; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\FileTypeMapper; +use function array_keys; +use function array_map; +use function array_merge; +use function count; +use function implode; +use function in_array; +use function is_int; +use function is_string; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt> + * @implements Rule */ class WrongVariableNameInVarTagRule implements Rule { - private FileTypeMapper $fileTypeMapper; - - private bool $checkWrongVarUsage; - public function __construct( - FileTypeMapper $fileTypeMapper, - bool $checkWrongVarUsage = false + private FileTypeMapper $fileTypeMapper, ) { - $this->fileTypeMapper = $fileTypeMapper; - $this->checkWrongVarUsage = $checkWrongVarUsage; } public function getNodeType(): string { - return \PhpParser\Node\Stmt::class; + return Node\Stmt::class; } public function processNode(Node $node, Scope $scope): array @@ -61,7 +66,7 @@ public function processNode(Node $node, Scope $scope): array $scope->isInClass() ? $scope->getClassReflection()->getName() : null, $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, $function !== null ? $function->getName() : null, - $comment->getText() + $comment->getText(), ); foreach ($resolvedPhpDoc->getVarTags() as $key => $varTag) { $varTags[$key] = $varTag; @@ -93,37 +98,34 @@ public function processNode(Node $node, Scope $scope): array } if ($node instanceof InClassNode || $node instanceof InClassMethodNode || $node instanceof InFunctionNode) { - if ($this->checkWrongVarUsage) { - $description = 'a function'; - $originalNode = $node->getOriginalNode(); - if ($originalNode instanceof Node\Stmt\Interface_) { - $description = 'an interface'; - } elseif ($originalNode instanceof Node\Stmt\Class_) { - $description = 'a class'; - } elseif ($originalNode instanceof Node\Stmt\Trait_) { - throw new \PHPStan\ShouldNotHappenException(); - } elseif ($originalNode instanceof Node\Stmt\ClassMethod) { - $description = 'a method'; - } - return [ - RuleErrorBuilder::message(sprintf( - 'PHPDoc tag @var above %s has no effect.', - $description - ))->build(), - ]; + $description = 'a function'; + $originalNode = $node->getOriginalNode(); + if ($originalNode instanceof Node\Stmt\Interface_) { + $description = 'an interface'; + } elseif ($originalNode instanceof Node\Stmt\Class_) { + $description = 'a class'; + } elseif ($originalNode instanceof Node\Stmt\Enum_) { + $description = 'an enum'; + } elseif ($originalNode instanceof Node\Stmt\Trait_) { + throw new ShouldNotHappenException(); + } elseif ($originalNode instanceof Node\Stmt\ClassMethod) { + $description = 'a method'; } - return []; + return [ + RuleErrorBuilder::message(sprintf( + 'PHPDoc tag @var above %s has no effect.', + $description, + ))->build(), + ]; } return $this->processStmt($scope, $varTags, null); } /** - * @param \PHPStan\Analyser\Scope $scope - * @param \PhpParser\Node\Expr $var - * @param \PHPStan\PhpDoc\Tag\VarTag[] $varTags - * @return \PHPStan\Rules\RuleError[] + * @param VarTag[] $varTags + * @return RuleError[] */ private function processAssign(Scope $scope, Node\Expr $var, array $varTags): array { @@ -139,7 +141,7 @@ private function processAssign(Scope $scope, Node\Expr $var, array $varTags): ar } } elseif (count($assignedVariables) !== 1) { $errors[] = RuleErrorBuilder::message( - 'PHPDoc tag @var above assignment does not specify variable name.' + 'PHPDoc tag @var above assignment does not specify variable name.', )->build(); } continue; @@ -157,7 +159,7 @@ private function processAssign(Scope $scope, Node\Expr $var, array $varTags): ar $errors[] = RuleErrorBuilder::message(sprintf( 'Variable $%s in PHPDoc tag @var does not match assigned variable $%s.', $key, - $assignedVariables[0] + $assignedVariables[0], ))->build(); } else { $errors[] = RuleErrorBuilder::message(sprintf('Variable $%s in PHPDoc tag @var does not exist.', $key))->build(); @@ -168,7 +170,6 @@ private function processAssign(Scope $scope, Node\Expr $var, array $varTags): ar } /** - * @param Expr $expr * @return string[] */ private function getAssignedVariables(Expr $expr): array @@ -198,10 +199,8 @@ private function getAssignedVariables(Expr $expr): array } /** - * @param \PhpParser\Node\Expr|null $keyVar - * @param \PhpParser\Node\Expr $valueVar - * @param \PHPStan\PhpDoc\Tag\VarTag[] $varTags - * @return \PHPStan\Rules\RuleError[] + * @param VarTag[] $varTags + * @return RuleError[] */ private function processForeach(Node\Expr $iterateeExpr, ?Node\Expr $keyVar, Node\Expr $valueVar, array $varTags): array { @@ -221,7 +220,7 @@ private function processForeach(Node\Expr $iterateeExpr, ?Node\Expr $keyVar, Nod continue; } $errors[] = RuleErrorBuilder::message( - 'PHPDoc tag @var above foreach loop does not specify variable name.' + 'PHPDoc tag @var above foreach loop does not specify variable name.', )->build(); continue; } @@ -233,9 +232,7 @@ private function processForeach(Node\Expr $iterateeExpr, ?Node\Expr $keyVar, Nod $errors[] = RuleErrorBuilder::message(sprintf( 'Variable $%s in PHPDoc tag @var does not match any variable in the foreach loop: %s', $name, - implode(', ', array_map(static function (string $name): string { - return sprintf('$%s', $name); - }, $variableNames)) + implode(', ', array_map(static fn (string $name): string => sprintf('$%s', $name), $variableNames)), ))->build(); } @@ -243,9 +240,9 @@ private function processForeach(Node\Expr $iterateeExpr, ?Node\Expr $keyVar, Nod } /** - * @param \PhpParser\Node\Stmt\StaticVar[] $vars - * @param \PHPStan\PhpDoc\Tag\VarTag[] $varTags - * @return \PHPStan\Rules\RuleError[] + * @param Node\Stmt\StaticVar[] $vars + * @param VarTag[] $varTags + * @return RuleError[] */ private function processStatic(array $vars, array $varTags): array { @@ -266,7 +263,7 @@ private function processStatic(array $vars, array $varTags): array } $errors[] = RuleErrorBuilder::message( - 'PHPDoc tag @var above multiple static variables does not specify variable name.' + 'PHPDoc tag @var above multiple static variables does not specify variable name.', )->build(); continue; } @@ -278,9 +275,7 @@ private function processStatic(array $vars, array $varTags): array $errors[] = RuleErrorBuilder::message(sprintf( 'Variable $%s in PHPDoc tag @var does not match any static variable: %s', $name, - implode(', ', array_map(static function (string $name): string { - return sprintf('$%s', $name); - }, array_keys($variableNames))) + implode(', ', array_map(static fn (string $name): string => sprintf('$%s', $name), array_keys($variableNames))), ))->build(); } @@ -288,10 +283,8 @@ private function processStatic(array $vars, array $varTags): array } /** - * @param \PHPStan\Analyser\Scope $scope - * @param \PhpParser\Node\Expr $expr - * @param \PHPStan\PhpDoc\Tag\VarTag[] $varTags - * @return \PHPStan\Rules\RuleError[] + * @param VarTag[] $varTags + * @return RuleError[] */ private function processExpression(Scope $scope, Expr $expr, array $varTags): array { @@ -303,10 +296,8 @@ private function processExpression(Scope $scope, Expr $expr, array $varTags): ar } /** - * @param \PHPStan\Analyser\Scope $scope - * @param \PHPStan\PhpDoc\Tag\VarTag[] $varTags - * @param Expr|null $defaultExpr - * @return \PHPStan\Rules\RuleError[] + * @param VarTag[] $varTags + * @return RuleError[] */ private function processStmt(Scope $scope, array $varTags, ?Expr $defaultExpr): array { @@ -336,9 +327,8 @@ private function processStmt(Scope $scope, array $varTags, ?Expr $defaultExpr): } /** - * @param \PHPStan\Analyser\Scope $scope - * @param \PHPStan\PhpDoc\Tag\VarTag[] $varTags - * @return \PHPStan\Rules\RuleError[] + * @param VarTag[] $varTags + * @return RuleError[] */ private function processGlobal(Scope $scope, Node\Stmt\Global_ $node, array $varTags): array { @@ -362,7 +352,7 @@ private function processGlobal(Scope $scope, Node\Stmt\Global_ $node, array $var } $errors[] = RuleErrorBuilder::message( - 'PHPDoc tag @var above multiple global variables does not specify variable name.' + 'PHPDoc tag @var above multiple global variables does not specify variable name.', )->build(); continue; } @@ -374,9 +364,7 @@ private function processGlobal(Scope $scope, Node\Stmt\Global_ $node, array $var $errors[] = RuleErrorBuilder::message(sprintf( 'Variable $%s in PHPDoc tag @var does not match any global variable: %s', $name, - implode(', ', array_map(static function (string $name): string { - return sprintf('$%s', $name); - }, array_keys($variableNames))) + implode(', ', array_map(static fn (string $name): string => sprintf('$%s', $name), array_keys($variableNames))), ))->build(); } diff --git a/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php b/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php new file mode 100644 index 0000000000..5f7e92fbac --- /dev/null +++ b/src/Rules/Properties/AccessPrivatePropertyThroughStaticRule.php @@ -0,0 +1,64 @@ + + */ +class AccessPrivatePropertyThroughStaticRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Expr\StaticPropertyFetch::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\VarLikeIdentifier) { + return []; + } + if (!$node->class instanceof Name) { + return []; + } + + $propertyName = $node->name->name; + $className = $node->class; + if ($className->toLowerString() !== 'static') { + return []; + } + + $classType = $scope->resolveTypeByName($className); + if (!$classType->hasProperty($propertyName)->yes()) { + return []; + } + + $property = $classType->getProperty($propertyName, $scope); + if (!$property->isPrivate()) { + return []; + } + if (!$property->isStatic()) { + return []; + } + + if ($scope->isInClass() && $scope->getClassReflection()->isFinal()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Unsafe access to private property %s::$%s through static::.', + $property->getDeclaringClass()->getDisplayName(), + $propertyName, + ))->build(), + ]; + } + +} diff --git a/src/Rules/Properties/AccessPropertiesInAssignRule.php b/src/Rules/Properties/AccessPropertiesInAssignRule.php index 2ed10a19fe..e5d3d08b4e 100644 --- a/src/Rules/Properties/AccessPropertiesInAssignRule.php +++ b/src/Rules/Properties/AccessPropertiesInAssignRule.php @@ -4,33 +4,35 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\PropertyAssignNode; use PHPStan\Rules\Rule; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Assign> + * @implements Rule */ class AccessPropertiesInAssignRule implements Rule { - private \PHPStan\Rules\Properties\AccessPropertiesRule $accessPropertiesRule; - - public function __construct(AccessPropertiesRule $accessPropertiesRule) + public function __construct(private AccessPropertiesRule $accessPropertiesRule) { - $this->accessPropertiesRule = $accessPropertiesRule; } public function getNodeType(): string { - return Node\Expr\Assign::class; + return PropertyAssignNode::class; } public function processNode(Node $node, Scope $scope): array { - if (!$node->var instanceof Node\Expr\PropertyFetch) { + if (!$node->getPropertyFetch() instanceof Node\Expr\PropertyFetch) { + return []; + } + + if ($node->isAssignOp()) { return []; } - return $this->accessPropertiesRule->processNode($node->var, $scope); + return $this->accessPropertiesRule->processNode($node->getPropertyFetch(), $scope); } } diff --git a/src/Rules/Properties/AccessPropertiesRule.php b/src/Rules/Properties/AccessPropertiesRule.php index 7ab6fe9409..5b3d81776a 100644 --- a/src/Rules/Properties/AccessPropertiesRule.php +++ b/src/Rules/Properties/AccessPropertiesRule.php @@ -2,10 +2,14 @@ namespace PHPStan\Rules\Properties; +use PhpParser\Node; use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Identifier; +use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; @@ -14,28 +18,23 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeUtils; use PHPStan\Type\VerbosityLevel; +use function array_map; +use function array_merge; +use function count; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\PropertyFetch> + * @implements Rule */ -class AccessPropertiesRule implements \PHPStan\Rules\Rule +class AccessPropertiesRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - private bool $reportMagicProperties; - public function __construct( - ReflectionProvider $reflectionProvider, - RuleLevelHelper $ruleLevelHelper, - bool $reportMagicProperties + private ReflectionProvider $reflectionProvider, + private RuleLevelHelper $ruleLevelHelper, + private bool $reportMagicProperties, ) { - $this->reflectionProvider = $reflectionProvider; - $this->ruleLevelHelper = $ruleLevelHelper; - $this->reportMagicProperties = $reportMagicProperties; } public function getNodeType(): string @@ -43,14 +42,12 @@ public function getNodeType(): string return PropertyFetch::class; } - public function processNode(\PhpParser\Node $node, Scope $scope): array + public function processNode(Node $node, Scope $scope): array { if ($node->name instanceof Identifier) { $names = [$node->name->name]; } else { - $names = array_map(static function (ConstantStringType $type): string { - return $type->getValue(); - }, TypeUtils::getConstantStrings($scope->getType($node->name))); + $names = array_map(static fn (ConstantStringType $type): string => $type->getValue(), TypeUtils::getConstantStrings($scope->getType($node->name))); } $errors = []; @@ -62,20 +59,15 @@ public function processNode(\PhpParser\Node $node, Scope $scope): array } /** - * @param Scope $scope - * @param PropertyFetch $node - * @param string $name * @return RuleError[] */ private function processSingleProperty(Scope $scope, PropertyFetch $node, string $name): array { $typeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, - $node->var, - sprintf('Access to property $%s on an unknown class %%s.', $name), - static function (Type $type) use ($name): bool { - return $type->canAccessProperties()->yes() && $type->hasProperty($name)->yes(); - } + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->var), + sprintf('Access to property $%s on an unknown class %%s.', SprintfHelper::escapeFormatString($name)), + static fn (Type $type): bool => $type->canAccessProperties()->yes() && $type->hasProperty($name)->yes(), ); $type = $typeResult->getType(); if ($type instanceof ErrorType) { @@ -91,7 +83,7 @@ static function (Type $type) use ($name): bool { RuleErrorBuilder::message(sprintf( 'Cannot access property $%s on %s.', $name, - $type->describe(VerbosityLevel::typeOnly()) + $type->describe(VerbosityLevel::typeOnly()), ))->build(), ]; } @@ -122,13 +114,13 @@ static function (Type $type) use ($name): bool { $referencedClass = $typeResult->getReferencedClasses()[0]; $propertyClassReflection = $this->reflectionProvider->getClass($referencedClass); $parentClassReflection = $propertyClassReflection->getParentClass(); - while ($parentClassReflection !== false) { + while ($parentClassReflection !== null) { if ($parentClassReflection->hasProperty($name)) { return [ RuleErrorBuilder::message(sprintf( 'Access to private property $%s of parent class %s.', $name, - $parentClassReflection->getDisplayName() + $parentClassReflection->getDisplayName(), ))->build(), ]; } @@ -137,12 +129,17 @@ static function (Type $type) use ($name): bool { } } + $ruleErrorBuilder = RuleErrorBuilder::message(sprintf( + 'Access to an undefined property %s::$%s.', + $type->describe(VerbosityLevel::typeOnly()), + $name, + )); + if ($typeResult->getTip() !== null) { + $ruleErrorBuilder->tip($typeResult->getTip()); + } + return [ - RuleErrorBuilder::message(sprintf( - 'Access to an undefined property %s::$%s.', - $type->describe(VerbosityLevel::typeOnly()), - $name - ))->build(), + $ruleErrorBuilder->build(), ]; } @@ -153,7 +150,7 @@ static function (Type $type) use ($name): bool { 'Access to %s property %s::$%s.', $propertyReflection->isPrivate() ? 'private' : 'protected', $type->describe(VerbosityLevel::typeOnly()), - $name + $name, ))->build(), ]; } diff --git a/src/Rules/Properties/AccessStaticPropertiesInAssignRule.php b/src/Rules/Properties/AccessStaticPropertiesInAssignRule.php index efd61a5c59..21d6ff3e1f 100644 --- a/src/Rules/Properties/AccessStaticPropertiesInAssignRule.php +++ b/src/Rules/Properties/AccessStaticPropertiesInAssignRule.php @@ -4,33 +4,35 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\PropertyAssignNode; use PHPStan\Rules\Rule; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Assign> + * @implements Rule */ class AccessStaticPropertiesInAssignRule implements Rule { - private \PHPStan\Rules\Properties\AccessStaticPropertiesRule $accessStaticPropertiesRule; - - public function __construct(AccessStaticPropertiesRule $accessStaticPropertiesRule) + public function __construct(private AccessStaticPropertiesRule $accessStaticPropertiesRule) { - $this->accessStaticPropertiesRule = $accessStaticPropertiesRule; } public function getNodeType(): string { - return Node\Expr\Assign::class; + return PropertyAssignNode::class; } public function processNode(Node $node, Scope $scope): array { - if (!$node->var instanceof Node\Expr\StaticPropertyFetch) { + if (!$node->getPropertyFetch() instanceof Node\Expr\StaticPropertyFetch) { + return []; + } + + if ($node->isAssignOp()) { return []; } - return $this->accessStaticPropertiesRule->processNode($node->var, $scope); + return $this->accessStaticPropertiesRule->processNode($node->getPropertyFetch(), $scope); } } diff --git a/src/Rules/Properties/AccessStaticPropertiesRule.php b/src/Rules/Properties/AccessStaticPropertiesRule.php index 4233156fdf..81da5c51ee 100644 --- a/src/Rules/Properties/AccessStaticPropertiesRule.php +++ b/src/Rules/Properties/AccessStaticPropertiesRule.php @@ -5,13 +5,17 @@ use PhpParser\Node; use PhpParser\Node\Expr\StaticPropertyFetch; use PhpParser\Node\Name; +use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassNameNodePair; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ErrorType; use PHPStan\Type\StringType; @@ -20,28 +24,24 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use PHPStan\Type\VerbosityLevel; +use function array_map; +use function array_merge; +use function in_array; +use function sprintf; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\StaticPropertyFetch> + * @implements Rule */ -class AccessStaticPropertiesRule implements \PHPStan\Rules\Rule +class AccessStaticPropertiesRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - public function __construct( - ReflectionProvider $reflectionProvider, - RuleLevelHelper $ruleLevelHelper, - ClassCaseSensitivityCheck $classCaseSensitivityCheck + private ReflectionProvider $reflectionProvider, + private RuleLevelHelper $ruleLevelHelper, + private ClassCaseSensitivityCheck $classCaseSensitivityCheck, ) { - $this->reflectionProvider = $reflectionProvider; - $this->ruleLevelHelper = $ruleLevelHelper; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; } public function getNodeType(): string @@ -54,9 +54,7 @@ public function processNode(Node $node, Scope $scope): array if ($node->name instanceof Node\VarLikeIdentifier) { $names = [$node->name->name]; } else { - $names = array_map(static function (ConstantStringType $type): string { - return $type->getValue(); - }, TypeUtils::getConstantStrings($scope->getType($node->name))); + $names = array_map(static fn (ConstantStringType $type): string => $type->getValue(), TypeUtils::getConstantStrings($scope->getType($node->name))); } $errors = []; @@ -68,9 +66,6 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param Scope $scope - * @param StaticPropertyFetch $node - * @param string $name * @return RuleError[] */ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, string $name): array @@ -85,7 +80,7 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, RuleErrorBuilder::message(sprintf( 'Accessing %s::$%s outside of class scope.', $class, - $name + $name, ))->build(), ]; } @@ -96,24 +91,24 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, RuleErrorBuilder::message(sprintf( 'Accessing %s::$%s outside of class scope.', $class, - $name + $name, ))->build(), ]; } - if ($scope->getClassReflection()->getParentClass() === false) { + if ($scope->getClassReflection()->getParentClass() === null) { return [ RuleErrorBuilder::message(sprintf( '%s::%s() accesses parent::$%s but %s does not extend any class.', $scope->getClassReflection()->getDisplayName(), $scope->getFunctionName(), $name, - $scope->getClassReflection()->getDisplayName() + $scope->getClassReflection()->getDisplayName(), ))->build(), ]; } if ($scope->getFunctionName() === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $currentMethodReflection = $scope->getClassReflection()->getNativeMethod($scope->getFunctionName()); @@ -133,7 +128,7 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, RuleErrorBuilder::message(sprintf( 'Access to static property $%s on an unknown class %s.', $name, - $class + $class, ))->discoveringSymbolsTip()->build(), ]; } else { @@ -145,11 +140,9 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, } else { $classTypeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, - $node->class, - sprintf('Access to static property $%s on an unknown class %%s.', $name), - static function (Type $type) use ($name): bool { - return $type->canAccessProperties()->yes() && $type->hasProperty($name)->yes(); - } + NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->class), + sprintf('Access to static property $%s on an unknown class %%s.', SprintfHelper::escapeFormatString($name)), + static fn (Type $type): bool => $type->canAccessProperties()->yes() && $type->hasProperty($name)->yes(), ); $classType = $classTypeResult->getType(); if ($classType instanceof ErrorType) { @@ -176,7 +169,7 @@ static function (Type $type) use ($name): bool { RuleErrorBuilder::message(sprintf( 'Cannot access static property $%s on %s.', $name, - $typeForDescribe->describe(VerbosityLevel::typeOnly()) + $typeForDescribe->describe(VerbosityLevel::typeOnly()), ))->build(), ]); } @@ -190,7 +183,7 @@ static function (Type $type) use ($name): bool { RuleErrorBuilder::message(sprintf( 'Access to an undefined static property %s::$%s.', $typeForDescribe->describe(VerbosityLevel::typeOnly()), - $name + $name, ))->build(), ]); } @@ -208,7 +201,7 @@ static function (Type $type) use ($name): bool { RuleErrorBuilder::message(sprintf( 'Static access to instance property %s::$%s.', $property->getDeclaringClass()->getDisplayName(), - $name + $name, ))->build(), ]); } @@ -219,7 +212,7 @@ static function (Type $type) use ($name): bool { 'Access to %s property $%s of class %s.', $property->isPrivate() ? 'private' : 'protected', $name, - $property->getDeclaringClass()->getDisplayName() + $property->getDeclaringClass()->getDisplayName(), ))->build(), ]); } diff --git a/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php b/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php index 24d929355e..b27e01de6b 100644 --- a/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php +++ b/src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php @@ -5,22 +5,22 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\ClassPropertyNode; +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; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\ClassPropertyNode> + * @implements Rule */ -class DefaultValueTypesAssignedToPropertiesRule implements \PHPStan\Rules\Rule +class DefaultValueTypesAssignedToPropertiesRule implements Rule { - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - public function __construct(RuleLevelHelper $ruleLevelHelper) + public function __construct(private RuleLevelHelper $ruleLevelHelper) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string @@ -31,7 +31,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $classReflection = $scope->getClassReflection(); @@ -61,7 +61,7 @@ public function processNode(Node $node, Scope $scope): array $classReflection->getDisplayName(), $node->getName(), $propertyType->describe($verbosityLevel), - $defaultValueType->describe($verbosityLevel) + $defaultValueType->describe($verbosityLevel), ))->build(), ]; } diff --git a/src/Rules/Properties/ExistingClassesInPropertiesRule.php b/src/Rules/Properties/ExistingClassesInPropertiesRule.php index 6a4c1f1cf1..1f4780b33b 100644 --- a/src/Rules/Properties/ExistingClassesInPropertiesRule.php +++ b/src/Rules/Properties/ExistingClassesInPropertiesRule.php @@ -5,36 +5,33 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\ClassPropertyNode; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassCaseSensitivityCheck; 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; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\ClassPropertyNode> + * @implements Rule */ -class ExistingClassesInPropertiesRule implements \PHPStan\Rules\Rule +class ExistingClassesInPropertiesRule implements Rule { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private \PHPStan\Rules\ClassCaseSensitivityCheck $classCaseSensitivityCheck; - - private bool $checkClassCaseSensitivity; - - private bool $checkThisOnly; - public function __construct( - ReflectionProvider $reflectionProvider, - ClassCaseSensitivityCheck $classCaseSensitivityCheck, - bool $checkClassCaseSensitivity, - bool $checkThisOnly + private ReflectionProvider $reflectionProvider, + private ClassCaseSensitivityCheck $classCaseSensitivityCheck, + private UnresolvableTypeHelper $unresolvableTypeHelper, + private PhpVersion $phpVersion, + private bool $checkClassCaseSensitivity, + private bool $checkThisOnly, ) { - $this->reflectionProvider = $reflectionProvider; - $this->classCaseSensitivityCheck = $classCaseSensitivityCheck; - $this->checkClassCaseSensitivity = $checkClassCaseSensitivity; - $this->checkThisOnly = $checkThisOnly; } public function getNodeType(): string @@ -45,7 +42,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $propertyReflection = $scope->getClassReflection()->getNativeProperty($node->getName()); @@ -54,7 +51,7 @@ public function processNode(Node $node, Scope $scope): array } else { $referencedClasses = array_merge( $propertyReflection->getNativeType()->getReferencedClasses(), - $propertyReflection->getPhpDocType()->getReferencedClasses() + $propertyReflection->getPhpDocType()->getReferencedClasses(), ); } @@ -66,7 +63,7 @@ public function processNode(Node $node, Scope $scope): array 'Property %s::$%s has invalid type %s.', $propertyReflection->getDeclaringClass()->getDisplayName(), $node->getName(), - $referencedClass + $referencedClass, ))->build(); } continue; @@ -76,19 +73,28 @@ public function processNode(Node $node, Scope $scope): array 'Property %s::$%s has unknown class %s as its type.', $propertyReflection->getDeclaringClass()->getDisplayName(), $node->getName(), - $referencedClass + $referencedClass, ))->discoveringSymbolsTip()->build(); } if ($this->checkClassCaseSensitivity) { $errors = array_merge( $errors, - $this->classCaseSensitivityCheck->checkClassNames(array_map(static function (string $class) use ($node): ClassNameNodePair { - return new ClassNameNodePair($class, $node); - }, $referencedClasses)) + $this->classCaseSensitivityCheck->checkClassNames(array_map(static fn (string $class): ClassNameNodePair => new ClassNameNodePair($class, $node), $referencedClasses)), ); } + if ( + $this->phpVersion->supportsPureIntersectionTypes() + && $this->unresolvableTypeHelper->containsUnresolvableType($propertyReflection->getNativeType()) + ) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Property %s::$%s has unresolvable native type.', + $propertyReflection->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->build(); + } + return $errors; } diff --git a/src/Rules/Properties/FoundPropertyReflection.php b/src/Rules/Properties/FoundPropertyReflection.php index 97efdcd412..0132ac0d3b 100644 --- a/src/Rules/Properties/FoundPropertyReflection.php +++ b/src/Rules/Properties/FoundPropertyReflection.php @@ -13,29 +13,14 @@ class FoundPropertyReflection implements PropertyReflection { - private PropertyReflection $originalPropertyReflection; - - private Scope $scope; - - private string $propertyName; - - private Type $readableType; - - private Type $writableType; - public function __construct( - PropertyReflection $originalPropertyReflection, - Scope $scope, - string $propertyName, - Type $readableType, - Type $writableType + private PropertyReflection $originalPropertyReflection, + private Scope $scope, + private string $propertyName, + private Type $readableType, + private Type $writableType, ) { - $this->originalPropertyReflection = $originalPropertyReflection; - $this->scope = $scope; - $this->propertyName = $propertyName; - $this->readableType = $readableType; - $this->writableType = $writableType; } public function getScope(): Scope @@ -115,15 +100,20 @@ public function isInternal(): TrinaryLogic public function isNative(): bool { - $reflection = $this->originalPropertyReflection; - while ($reflection instanceof WrapperPropertyReflection) { - $reflection = $reflection->getOriginalReflection(); + return $this->getNativeReflection() !== null; + } + + public function getNativeType(): ?Type + { + $reflection = $this->getNativeReflection(); + if ($reflection === null) { + return null; } - return $reflection instanceof PhpPropertyReflection; + return $reflection->getNativeType(); } - public function getNativeType(): ?Type + public function getNativeReflection(): ?PhpPropertyReflection { $reflection = $this->originalPropertyReflection; while ($reflection instanceof WrapperPropertyReflection) { @@ -134,7 +124,7 @@ public function getNativeType(): ?Type return null; } - return $reflection->getNativeType(); + return $reflection; } } diff --git a/src/Rules/Properties/LazyReadWritePropertiesExtensionProvider.php b/src/Rules/Properties/LazyReadWritePropertiesExtensionProvider.php index eed89ae937..3f61fd0fb8 100644 --- a/src/Rules/Properties/LazyReadWritePropertiesExtensionProvider.php +++ b/src/Rules/Properties/LazyReadWritePropertiesExtensionProvider.php @@ -7,14 +7,11 @@ class LazyReadWritePropertiesExtensionProvider implements ReadWritePropertiesExtensionProvider { - private Container $container; - /** @var ReadWritePropertiesExtension[]|null */ private ?array $extensions = null; - public function __construct(Container $container) + public function __construct(private Container $container) { - $this->container = $container; } public function getExtensions(): array diff --git a/src/Rules/Properties/MissingPropertyTypehintRule.php b/src/Rules/Properties/MissingPropertyTypehintRule.php index f45f29ce7a..6831c3ca37 100644 --- a/src/Rules/Properties/MissingPropertyTypehintRule.php +++ b/src/Rules/Properties/MissingPropertyTypehintRule.php @@ -6,21 +6,22 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\ClassPropertyNode; 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; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\ClassPropertyNode> + * @implements Rule */ -final class MissingPropertyTypehintRule implements \PHPStan\Rules\Rule +final class MissingPropertyTypehintRule implements Rule { - private \PHPStan\Rules\MissingTypehintCheck $missingTypehintCheck; - - public function __construct(MissingTypehintCheck $missingTypehintCheck) + public function __construct(private MissingTypehintCheck $missingTypehintCheck) { - $this->missingTypehintCheck = $missingTypehintCheck; } public function getNodeType(): string @@ -31,7 +32,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $propertyReflection = $scope->getClassReflection()->getNativeProperty($node->getName()); @@ -39,9 +40,9 @@ public function processNode(Node $node, Scope $scope): array if ($propertyType instanceof MixedType && !$propertyType->isExplicitMixed()) { return [ RuleErrorBuilder::message(sprintf( - 'Property %s::$%s has no typehint specified.', + 'Property %s::$%s has no type specified.', $propertyReflection->getDeclaringClass()->getDisplayName(), - $node->getName() + $node->getName(), ))->build(), ]; } @@ -53,7 +54,7 @@ public function processNode(Node $node, Scope $scope): array 'Property %s::$%s type has no value type specified in iterable type %s.', $propertyReflection->getDeclaringClass()->getDisplayName(), $node->getName(), - $iterableTypeDescription + $iterableTypeDescription, ))->tip(MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP)->build(); } @@ -63,7 +64,7 @@ public function processNode(Node $node, Scope $scope): array $propertyReflection->getDeclaringClass()->getDisplayName(), $node->getName(), $name, - implode(', ', $genericTypeNames) + implode(', ', $genericTypeNames), ))->tip(MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP)->build(); } @@ -72,7 +73,7 @@ public function processNode(Node $node, Scope $scope): array 'Property %s::$%s type has no signature specified for %s.', $propertyReflection->getDeclaringClass()->getDisplayName(), $node->getName(), - $callableType->describe(VerbosityLevel::typeOnly()) + $callableType->describe(VerbosityLevel::typeOnly()), ))->build(); } diff --git a/src/Rules/Properties/MissingReadOnlyPropertyAssignRule.php b/src/Rules/Properties/MissingReadOnlyPropertyAssignRule.php new file mode 100644 index 0000000000..ebe5d6231d --- /dev/null +++ b/src/Rules/Properties/MissingReadOnlyPropertyAssignRule.php @@ -0,0 +1,127 @@ + + */ +class MissingReadOnlyPropertyAssignRule implements Rule +{ + + /** @var array */ + private array $additionalConstructorsCache = []; + + /** + * @param string[] $additionalConstructors + */ + public function __construct( + private array $additionalConstructors, + ) + { + } + + public function getNodeType(): string + { + return ClassPropertiesNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } + $classReflection = $scope->getClassReflection(); + [$properties, $prematureAccess, $additionalAssigns] = $node->getUninitializedProperties($scope, $this->getConstructors($classReflection), []); + + $errors = []; + foreach ($properties as $propertyName => $propertyNode) { + if (!$propertyNode->isReadOnly()) { + continue; + } + $errors[] = RuleErrorBuilder::message(sprintf( + 'Class %s has an uninitialized readonly property $%s. Assign it in the constructor.', + $classReflection->getDisplayName(), + $propertyName, + ))->line($propertyNode->getLine())->build(); + } + + foreach ($prematureAccess as [$propertyName, $line, $propertyNode]) { + if (!$propertyNode->isReadOnly()) { + continue; + } + $errors[] = RuleErrorBuilder::message(sprintf( + 'Access to an uninitialized readonly property %s::$%s.', + $classReflection->getDisplayName(), + $propertyName, + ))->line($line)->build(); + } + + foreach ($additionalAssigns as [$propertyName, $line, $propertyNode]) { + if (!$propertyNode->isReadOnly()) { + continue; + } + $errors[] = RuleErrorBuilder::message(sprintf( + 'Readonly property %s::$%s is already assigned.', + $classReflection->getDisplayName(), + $propertyName, + ))->line($line)->build(); + } + + return $errors; + } + + /** + * @return string[] + */ + private function getConstructors(ClassReflection $classReflection): array + { + if (array_key_exists($classReflection->getName(), $this->additionalConstructorsCache)) { + return $this->additionalConstructorsCache[$classReflection->getName()]; + } + $constructors = []; + if ($classReflection->hasConstructor()) { + $constructors[] = $classReflection->getConstructor()->getName(); + } + + $nativeReflection = $classReflection->getNativeReflection(); + foreach ($this->additionalConstructors as $additionalConstructor) { + [$className, $methodName] = explode('::', $additionalConstructor); + if (!$nativeReflection->hasMethod($methodName)) { + continue; + } + $nativeMethod = $nativeReflection->getMethod($methodName); + if ($nativeMethod->getDeclaringClass()->getName() !== $nativeReflection->getName()) { + continue; + } + + try { + $prototype = $nativeMethod->getPrototype(); + } catch (ReflectionException) { + $prototype = $nativeMethod; + } + + if ($prototype->getDeclaringClass()->getName() !== $className) { + continue; + } + + $constructors[] = $methodName; + } + + $this->additionalConstructorsCache[$classReflection->getName()] = $constructors; + + return $constructors; + } + +} diff --git a/src/Rules/Properties/NullsafePropertyFetchRule.php b/src/Rules/Properties/NullsafePropertyFetchRule.php index 803eabf0f2..a71d24f45d 100644 --- a/src/Rules/Properties/NullsafePropertyFetchRule.php +++ b/src/Rules/Properties/NullsafePropertyFetchRule.php @@ -8,6 +8,7 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\NullType; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** * @implements Rule diff --git a/src/Rules/Properties/OverridingPropertyRule.php b/src/Rules/Properties/OverridingPropertyRule.php new file mode 100644 index 0000000000..e8749b62f0 --- /dev/null +++ b/src/Rules/Properties/OverridingPropertyRule.php @@ -0,0 +1,225 @@ + + */ +class OverridingPropertyRule implements Rule +{ + + public function __construct( + private bool $checkPhpDocMethodSignatures, + private bool $reportMaybes, + ) + { + } + + public function getNodeType(): string + { + return ClassPropertyNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$scope->isInClass()) { + throw new ShouldNotHappenException(); + } + + $classReflection = $scope->getClassReflection(); + $prototype = $this->findPrototype($classReflection, $node->getName()); + if ($prototype === null) { + return []; + } + + $errors = []; + if ($prototype->isStatic()) { + if (!$node->isStatic()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Non-static property %s::$%s overrides static property %s::$%s.', + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->nonIgnorable()->build(); + } + } elseif ($node->isStatic()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Static property %s::$%s overrides non-static property %s::$%s.', + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->nonIgnorable()->build(); + } + + if ($prototype->isReadOnly()) { + if (!$node->isReadOnly()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Readwrite property %s::$%s overrides readonly property %s::$%s.', + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->nonIgnorable()->build(); + } + } elseif ($node->isReadOnly()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Readonly property %s::$%s overrides readwrite property %s::$%s.', + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->nonIgnorable()->build(); + } + + if ($prototype->isPublic()) { + if (!$node->isPublic()) { + $errors[] = RuleErrorBuilder::message(sprintf( + '%s property %s::$%s overriding public property %s::$%s should also be public.', + $node->isPrivate() ? 'Private' : 'Protected', + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->nonIgnorable()->build(); + } + } elseif ($node->isPrivate()) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Private property %s::$%s overriding protected property %s::$%s should be protected or public.', + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->nonIgnorable()->build(); + } + + $typeErrors = []; + if ($prototype->hasNativeType()) { + if ($node->getNativeType() === null) { + $typeErrors[] = RuleErrorBuilder::message(sprintf( + 'Property %s::$%s overriding property %s::$%s (%s) should also have native type %s.', + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + $prototype->getNativeType()->describe(VerbosityLevel::typeOnly()), + $prototype->getNativeType()->describe(VerbosityLevel::typeOnly()), + ))->nonIgnorable()->build(); + } else { + $nativeType = ParserNodeTypeToPHPStanType::resolve($node->getNativeType(), $scope->getClassReflection()); + 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.', + $nativeType->describe(VerbosityLevel::typeOnly()), + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getNativeType()->describe(VerbosityLevel::typeOnly()), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->nonIgnorable()->build(); + } + } + } elseif ($node->getNativeType() !== null) { + $typeErrors[] = RuleErrorBuilder::message(sprintf( + '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()), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->nonIgnorable()->build(); + } + + $errors = array_merge($errors, $typeErrors); + + if (!$this->checkPhpDocMethodSignatures) { + return $errors; + } + + if (count($typeErrors) > 0) { + return $errors; + } + + $propertyReflection = $classReflection->getNativeProperty($node->getName()); + if ($prototype->getReadableType()->equals($propertyReflection->getReadableType())) { + return $errors; + } + + $verbosity = VerbosityLevel::getRecommendedLevelByType($prototype->getReadableType(), $propertyReflection->getReadableType()); + $isSuperType = $prototype->getReadableType()->isSuperTypeOf($propertyReflection->getReadableType()); + $canBeTurnedOffError = RuleErrorBuilder::message(sprintf( + 'PHPDoc type %s of property %s::$%s is not the same as PHPDoc type %s of overridden property %s::$%s.', + $propertyReflection->getReadableType()->describe($verbosity), + $classReflection->getDisplayName(), + $node->getName(), + $prototype->getReadableType()->describe($verbosity), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->tip(sprintf( + "You can fix 3rd party PHPDoc types with stub files:\n %s\n This error can be turned off by setting\n %s", + 'https://phpstan.org/user-guide/stub-files', + 'reportMaybesInPropertyPhpDocTypes: false in your %configurationFile%.', + ))->build(); + $cannotBeTurnedOffError = RuleErrorBuilder::message(sprintf( + 'PHPDoc type %s of property %s::$%s is %s PHPDoc type %s of overridden property %s::$%s.', + $propertyReflection->getReadableType()->describe($verbosity), + $classReflection->getDisplayName(), + $node->getName(), + $this->reportMaybes ? 'not the same as' : 'not covariant with', + $prototype->getReadableType()->describe($verbosity), + $prototype->getDeclaringClass()->getDisplayName(), + $node->getName(), + ))->tip(sprintf( + "You can fix 3rd party PHPDoc types with stub files:\n %s", + 'https://phpstan.org/user-guide/stub-files', + ))->build(); + if ($this->reportMaybes) { + if (!$isSuperType->yes()) { + $errors[] = $cannotBeTurnedOffError; + } else { + $errors[] = $canBeTurnedOffError; + } + } else { + if (!$isSuperType->yes()) { + $errors[] = $cannotBeTurnedOffError; + } + } + + return $errors; + } + + private function findPrototype(ClassReflection $classReflection, string $propertyName): ?PhpPropertyReflection + { + $parentClass = $classReflection->getParentClass(); + if ($parentClass === null) { + return null; + } + + if (!$parentClass->hasNativeProperty($propertyName)) { + return null; + } + + $property = $parentClass->getNativeProperty($propertyName); + if ($property->isPrivate()) { + return null; + } + + return $property; + } + +} diff --git a/src/Rules/Properties/PropertyAttributesRule.php b/src/Rules/Properties/PropertyAttributesRule.php index d00a080e8f..b0d9cf2812 100644 --- a/src/Rules/Properties/PropertyAttributesRule.php +++ b/src/Rules/Properties/PropertyAttributesRule.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Properties; +use Attribute; use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Rules\AttributesCheck; @@ -13,11 +14,8 @@ class PropertyAttributesRule implements Rule { - private AttributesCheck $attributesCheck; - - public function __construct(AttributesCheck $attributesCheck) + public function __construct(private AttributesCheck $attributesCheck) { - $this->attributesCheck = $attributesCheck; } public function getNodeType(): string @@ -30,8 +28,8 @@ public function processNode(Node $node, Scope $scope): array return $this->attributesCheck->check( $scope, $node->attrGroups, - \Attribute::TARGET_PROPERTY, - 'property' + Attribute::TARGET_PROPERTY, + 'property', ); } diff --git a/src/Rules/Properties/PropertyDescriptor.php b/src/Rules/Properties/PropertyDescriptor.php index e8e1a74a24..4bd700c718 100644 --- a/src/Rules/Properties/PropertyDescriptor.php +++ b/src/Rules/Properties/PropertyDescriptor.php @@ -2,7 +2,9 @@ namespace PHPStan\Rules\Properties; +use PhpParser\Node; use PHPStan\Reflection\PropertyReflection; +use function sprintf; class PropertyDescriptor { @@ -17,13 +19,11 @@ public function describePropertyByName(PropertyReflection $property, string $pro } /** - * @param \PHPStan\Reflection\PropertyReflection $property - * @param \PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\StaticPropertyFetch $propertyFetch - * @return string + * @param Node\Expr\PropertyFetch|Node\Expr\StaticPropertyFetch $propertyFetch */ public function describeProperty(PropertyReflection $property, $propertyFetch): string { - /** @var \PhpParser\Node\Identifier $name */ + /** @var Node\Identifier $name */ $name = $propertyFetch->name; if (!$property->isStatic()) { return sprintf('Property %s::$%s', $property->getDeclaringClass()->getDisplayName(), $name->name); diff --git a/src/Rules/Properties/PropertyReflectionFinder.php b/src/Rules/Properties/PropertyReflectionFinder.php index 3e07277ada..1dd0cc4748 100644 --- a/src/Rules/Properties/PropertyReflectionFinder.php +++ b/src/Rules/Properties/PropertyReflectionFinder.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Properties; +use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\Node\Scalar\String_; use PhpParser\Node\VarLikeIdentifier; @@ -9,24 +10,22 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Type; use PHPStan\Type\TypeUtils; +use function array_map; class PropertyReflectionFinder { /** - * @param \PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\StaticPropertyFetch $propertyFetch - * @param \PHPStan\Analyser\Scope $scope + * @param Node\Expr\PropertyFetch|Node\Expr\StaticPropertyFetch $propertyFetch * @return FoundPropertyReflection[] */ public function findPropertyReflectionsFromNode($propertyFetch, Scope $scope): array { - if ($propertyFetch instanceof \PhpParser\Node\Expr\PropertyFetch) { - if ($propertyFetch->name instanceof \PhpParser\Node\Identifier) { + if ($propertyFetch instanceof Node\Expr\PropertyFetch) { + if ($propertyFetch->name instanceof Node\Identifier) { $names = [$propertyFetch->name->name]; } else { - $names = array_map(static function (ConstantStringType $name): string { - return $name->getValue(); - }, TypeUtils::getConstantStrings($scope->getType($propertyFetch->name))); + $names = array_map(static fn (ConstantStringType $name): string => $name->getValue(), TypeUtils::getConstantStrings($scope->getType($propertyFetch->name))); } $reflections = []; @@ -37,8 +36,8 @@ public function findPropertyReflectionsFromNode($propertyFetch, Scope $scope): a $name, $propertyFetch->name instanceof Expr ? $scope->filterByTruthyValue(new Expr\BinaryOp\Identical( $propertyFetch->name, - new String_($name) - )) : $scope + new String_($name), + )) : $scope, ); if ($reflection === null) { continue; @@ -50,7 +49,7 @@ public function findPropertyReflectionsFromNode($propertyFetch, Scope $scope): a return $reflections; } - if ($propertyFetch->class instanceof \PhpParser\Node\Name) { + if ($propertyFetch->class instanceof Node\Name) { $propertyHolderType = $scope->resolveTypeByName($propertyFetch->class); } else { $propertyHolderType = $scope->getType($propertyFetch->class); @@ -59,9 +58,7 @@ public function findPropertyReflectionsFromNode($propertyFetch, Scope $scope): a if ($propertyFetch->name instanceof VarLikeIdentifier) { $names = [$propertyFetch->name->name]; } else { - $names = array_map(static function (ConstantStringType $name): string { - return $name->getValue(); - }, TypeUtils::getConstantStrings($scope->getType($propertyFetch->name))); + $names = array_map(static fn (ConstantStringType $name): string => $name->getValue(), TypeUtils::getConstantStrings($scope->getType($propertyFetch->name))); } $reflections = []; @@ -71,8 +68,8 @@ public function findPropertyReflectionsFromNode($propertyFetch, Scope $scope): a $name, $propertyFetch->name instanceof Expr ? $scope->filterByTruthyValue(new Expr\BinaryOp\Identical( $propertyFetch->name, - new String_($name) - )) : $scope + new String_($name), + )) : $scope, ); if ($reflection === null) { continue; @@ -85,25 +82,23 @@ public function findPropertyReflectionsFromNode($propertyFetch, Scope $scope): a } /** - * @param \PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\StaticPropertyFetch $propertyFetch - * @param \PHPStan\Analyser\Scope $scope - * @return FoundPropertyReflection|null + * @param Node\Expr\PropertyFetch|Node\Expr\StaticPropertyFetch $propertyFetch */ public function findPropertyReflectionFromNode($propertyFetch, Scope $scope): ?FoundPropertyReflection { - if ($propertyFetch instanceof \PhpParser\Node\Expr\PropertyFetch) { - if (!$propertyFetch->name instanceof \PhpParser\Node\Identifier) { + if ($propertyFetch instanceof Node\Expr\PropertyFetch) { + if (!$propertyFetch->name instanceof Node\Identifier) { return null; } $propertyHolderType = $scope->getType($propertyFetch->var); return $this->findPropertyReflection($propertyHolderType, $propertyFetch->name->name, $scope); } - if (!$propertyFetch->name instanceof \PhpParser\Node\Identifier) { + if (!$propertyFetch->name instanceof Node\Identifier) { return null; } - if ($propertyFetch->class instanceof \PhpParser\Node\Name) { + if ($propertyFetch->class instanceof Node\Name) { $propertyHolderType = $scope->resolveTypeByName($propertyFetch->class); } else { $propertyHolderType = $scope->getType($propertyFetch->class); @@ -125,7 +120,7 @@ private function findPropertyReflection(Type $propertyHolderType, string $proper $scope, $propertyName, $originalProperty->getReadableType(), - $originalProperty->getWritableType() + $originalProperty->getWritableType(), ); } diff --git a/src/Rules/Properties/ReadOnlyPropertyAssignRefRule.php b/src/Rules/Properties/ReadOnlyPropertyAssignRefRule.php new file mode 100644 index 0000000000..97c5686c69 --- /dev/null +++ b/src/Rules/Properties/ReadOnlyPropertyAssignRefRule.php @@ -0,0 +1,55 @@ + + */ +class ReadOnlyPropertyAssignRefRule implements Rule +{ + + public function __construct(private PropertyReflectionFinder $propertyReflectionFinder) + { + } + + public function getNodeType(): string + { + return Node\Expr\AssignRef::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->expr instanceof Node\Expr\PropertyFetch && !$node->expr instanceof Node\Expr\StaticPropertyFetch) { + return []; + } + + $propertyFetch = $node->expr; + + $errors = []; + $reflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($propertyFetch, $scope); + foreach ($reflections as $propertyReflection) { + $nativeReflection = $propertyReflection->getNativeReflection(); + if ($nativeReflection === null) { + continue; + } + if (!$scope->canAccessProperty($propertyReflection)) { + continue; + } + if (!$nativeReflection->isReadOnly()) { + continue; + } + + $declaringClass = $nativeReflection->getDeclaringClass(); + $errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is assigned by reference.', $declaringClass->getDisplayName(), $propertyReflection->getName()))->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Properties/ReadOnlyPropertyAssignRule.php b/src/Rules/Properties/ReadOnlyPropertyAssignRule.php new file mode 100644 index 0000000000..e63953bc1d --- /dev/null +++ b/src/Rules/Properties/ReadOnlyPropertyAssignRule.php @@ -0,0 +1,84 @@ + + */ +class ReadOnlyPropertyAssignRule implements Rule +{ + + public function __construct(private PropertyReflectionFinder $propertyReflectionFinder) + { + } + + public function getNodeType(): string + { + return PropertyAssignNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $propertyFetch = $node->getPropertyFetch(); + if (!$propertyFetch instanceof Node\Expr\PropertyFetch) { + return []; + } + + $errors = []; + $reflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($propertyFetch, $scope); + foreach ($reflections as $propertyReflection) { + $nativeReflection = $propertyReflection->getNativeReflection(); + if ($nativeReflection === null) { + continue; + } + if (!$scope->canAccessProperty($propertyReflection)) { + continue; + } + if (!$nativeReflection->isReadOnly()) { + continue; + } + + $declaringClass = $nativeReflection->getDeclaringClass(); + + if (!$scope->isInClass()) { + $errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is assigned outside of its declaring class.', $declaringClass->getDisplayName(), $propertyReflection->getName()))->build(); + continue; + } + + $scopeClassReflection = $scope->getClassReflection(); + if ($scopeClassReflection->getName() !== $declaringClass->getName()) { + $errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is assigned outside of its declaring class.', $declaringClass->getDisplayName(), $propertyReflection->getName()))->build(); + continue; + } + + $scopeMethod = $scope->getFunction(); + if (!$scopeMethod instanceof MethodReflection) { + throw new ShouldNotHappenException(); + } + + if (strtolower($scopeMethod->getName()) === '__construct' || strtolower($scopeMethod->getName()) === '__unserialize') { + if (!$scope->getType($propertyFetch->var) instanceof ThisType) { + $errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is not assigned on $this.', $declaringClass->getDisplayName(), $propertyReflection->getName()))->build(); + } + + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is assigned outside of the constructor.', $declaringClass->getDisplayName(), $propertyReflection->getName()))->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Properties/ReadOnlyPropertyRule.php b/src/Rules/Properties/ReadOnlyPropertyRule.php new file mode 100644 index 0000000000..5c80d413ea --- /dev/null +++ b/src/Rules/Properties/ReadOnlyPropertyRule.php @@ -0,0 +1,49 @@ + + */ +class ReadOnlyPropertyRule implements Rule +{ + + public function __construct(private PhpVersion $phpVersion) + { + } + + public function getNodeType(): string + { + return ClassPropertyNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->isReadOnly()) { + return []; + } + + $errors = []; + if (!$this->phpVersion->supportsReadOnlyProperties()) { + $errors[] = RuleErrorBuilder::message('Readonly properties are supported only on PHP 8.1 and later.')->nonIgnorable()->build(); + } + + if ($node->getNativeType() === null) { + $errors[] = RuleErrorBuilder::message('Readonly property must have a native type.')->nonIgnorable()->build(); + } + + if ($node->getDefault() !== null) { + $errors[] = RuleErrorBuilder::message('Readonly property cannot have a default value.')->nonIgnorable()->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Properties/ReadingWriteOnlyPropertiesRule.php b/src/Rules/Properties/ReadingWriteOnlyPropertiesRule.php index 2b47df73bd..315be25535 100644 --- a/src/Rules/Properties/ReadingWriteOnlyPropertiesRule.php +++ b/src/Rules/Properties/ReadingWriteOnlyPropertiesRule.php @@ -4,39 +4,29 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr> + * @implements Rule */ -class ReadingWriteOnlyPropertiesRule implements \PHPStan\Rules\Rule +class ReadingWriteOnlyPropertiesRule implements Rule { - private \PHPStan\Rules\Properties\PropertyDescriptor $propertyDescriptor; - - private \PHPStan\Rules\Properties\PropertyReflectionFinder $propertyReflectionFinder; - - private RuleLevelHelper $ruleLevelHelper; - - private bool $checkThisOnly; - public function __construct( - PropertyDescriptor $propertyDescriptor, - PropertyReflectionFinder $propertyReflectionFinder, - RuleLevelHelper $ruleLevelHelper, - bool $checkThisOnly + private PropertyDescriptor $propertyDescriptor, + private PropertyReflectionFinder $propertyReflectionFinder, + private RuleLevelHelper $ruleLevelHelper, + private bool $checkThisOnly, ) { - $this->propertyDescriptor = $propertyDescriptor; - $this->propertyReflectionFinder = $propertyReflectionFinder; - $this->ruleLevelHelper = $ruleLevelHelper; - $this->checkThisOnly = $checkThisOnly; } public function getNodeType(): string { - return \PhpParser\Node\Expr::class; + return Node\Expr::class; } public function processNode(Node $node, Scope $scope): array @@ -74,7 +64,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message(sprintf( '%s is not readable.', - $propertyDescription + $propertyDescription, ))->build(), ]; } diff --git a/src/Rules/Properties/TypesAssignedToPropertiesRule.php b/src/Rules/Properties/TypesAssignedToPropertiesRule.php index 1a123ffdea..e86865cb29 100644 --- a/src/Rules/Properties/TypesAssignedToPropertiesRule.php +++ b/src/Rules/Properties/TypesAssignedToPropertiesRule.php @@ -4,65 +4,43 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\PropertyAssignNode; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\VerbosityLevel; +use function array_merge; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr> + * @implements Rule */ -class TypesAssignedToPropertiesRule implements \PHPStan\Rules\Rule +class TypesAssignedToPropertiesRule implements Rule { - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - private \PHPStan\Rules\Properties\PropertyDescriptor $propertyDescriptor; - - private \PHPStan\Rules\Properties\PropertyReflectionFinder $propertyReflectionFinder; - public function __construct( - RuleLevelHelper $ruleLevelHelper, - PropertyDescriptor $propertyDescriptor, - PropertyReflectionFinder $propertyReflectionFinder + private RuleLevelHelper $ruleLevelHelper, + private PropertyDescriptor $propertyDescriptor, + private PropertyReflectionFinder $propertyReflectionFinder, ) { - $this->ruleLevelHelper = $ruleLevelHelper; - $this->propertyDescriptor = $propertyDescriptor; - $this->propertyReflectionFinder = $propertyReflectionFinder; } public function getNodeType(): string { - return \PhpParser\Node\Expr::class; + return PropertyAssignNode::class; } public function processNode(Node $node, Scope $scope): array { - 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 []; - } - - /** @var \PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\StaticPropertyFetch $propertyFetch */ - $propertyFetch = $node->var; - $propertyReflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($propertyFetch, $scope); + $propertyReflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($node->getPropertyFetch(), $scope); $errors = []; foreach ($propertyReflections as $propertyReflection) { $errors = array_merge($errors, $this->processSingleProperty( $propertyReflection, - $node + $node->getAssignedExpr(), )); } @@ -70,23 +48,17 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param FoundPropertyReflection $propertyReflection - * @param Node\Expr $node * @return RuleError[] */ private function processSingleProperty( FoundPropertyReflection $propertyReflection, - Node\Expr $node + Node\Expr $assignedExpr, ): array { $propertyType = $propertyReflection->getWritableType(); $scope = $propertyReflection->getScope(); + $assignedValueType = $scope->getType($assignedExpr); - if ($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignRef) { - $assignedValueType = $scope->getType($node->expr); - } else { - $assignedValueType = $scope->getType($node); - } if (!$this->ruleLevelHelper->accepts($propertyType, $assignedValueType, $scope->isDeclareStrictTypes())) { $propertyDescription = $this->propertyDescriptor->describePropertyByName($propertyReflection, $propertyReflection->getName()); $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($propertyType, $assignedValueType); @@ -96,7 +68,7 @@ private function processSingleProperty( '%s (%s) does not accept %s.', $propertyDescription, $propertyType->describe($verbosityLevel), - $assignedValueType->describe($verbosityLevel) + $assignedValueType->describe($verbosityLevel), ))->build(), ]; } diff --git a/src/Rules/Properties/UninitializedPropertyRule.php b/src/Rules/Properties/UninitializedPropertyRule.php index ca4d9a8118..538bd5ef55 100644 --- a/src/Rules/Properties/UninitializedPropertyRule.php +++ b/src/Rules/Properties/UninitializedPropertyRule.php @@ -8,6 +8,11 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; +use ReflectionException; +use function array_key_exists; +use function explode; +use function sprintf; /** * @implements Rule @@ -15,11 +20,6 @@ class UninitializedPropertyRule implements Rule { - private ReadWritePropertiesExtensionProvider $extensionProvider; - - /** @var string[] */ - private array $additionalConstructors; - /** @var array */ private array $additionalConstructorsCache = []; @@ -27,12 +27,10 @@ class UninitializedPropertyRule implements Rule * @param string[] $additionalConstructors */ public function __construct( - ReadWritePropertiesExtensionProvider $extensionProvider, - array $additionalConstructors + private ReadWritePropertiesExtensionProvider $extensionProvider, + private array $additionalConstructors, ) { - $this->extensionProvider = $extensionProvider; - $this->additionalConstructors = $additionalConstructors; } public function getNodeType(): string @@ -43,25 +41,31 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { if (!$scope->isInClass()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $classReflection = $scope->getClassReflection(); [$properties, $prematureAccess] = $node->getUninitializedProperties($scope, $this->getConstructors($classReflection), $this->extensionProvider->getExtensions()); $errors = []; foreach ($properties as $propertyName => $propertyNode) { + if ($propertyNode->isReadOnly()) { + continue; + } $errors[] = RuleErrorBuilder::message(sprintf( 'Class %s has an uninitialized property $%s. Give it default value or assign it in the constructor.', $classReflection->getDisplayName(), - $propertyName + $propertyName, ))->line($propertyNode->getLine())->build(); } - foreach ($prematureAccess as [$propertyName, $line]) { + foreach ($prematureAccess as [$propertyName, $line, $propertyNode]) { + if ($propertyNode->isReadOnly()) { + continue; + } $errors[] = RuleErrorBuilder::message(sprintf( 'Access to an uninitialized property %s::$%s.', $classReflection->getDisplayName(), - $propertyName + $propertyName, ))->line($line)->build(); } @@ -69,7 +73,6 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param ClassReflection $classReflection * @return string[] */ private function getConstructors(ClassReflection $classReflection): array @@ -95,7 +98,7 @@ private function getConstructors(ClassReflection $classReflection): array try { $prototype = $nativeMethod->getPrototype(); - } catch (\ReflectionException $e) { + } catch (ReflectionException) { $prototype = $nativeMethod; } diff --git a/src/Rules/Properties/WritingToReadOnlyPropertiesRule.php b/src/Rules/Properties/WritingToReadOnlyPropertiesRule.php index ef79346ab0..f25cd413c7 100644 --- a/src/Rules/Properties/WritingToReadOnlyPropertiesRule.php +++ b/src/Rules/Properties/WritingToReadOnlyPropertiesRule.php @@ -4,39 +4,29 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr> + * @implements Rule */ -class WritingToReadOnlyPropertiesRule implements \PHPStan\Rules\Rule +class WritingToReadOnlyPropertiesRule implements Rule { - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - private \PHPStan\Rules\Properties\PropertyDescriptor $propertyDescriptor; - - private \PHPStan\Rules\Properties\PropertyReflectionFinder $propertyReflectionFinder; - - private bool $checkThisOnly; - public function __construct( - RuleLevelHelper $ruleLevelHelper, - PropertyDescriptor $propertyDescriptor, - PropertyReflectionFinder $propertyReflectionFinder, - bool $checkThisOnly + private RuleLevelHelper $ruleLevelHelper, + private PropertyDescriptor $propertyDescriptor, + private PropertyReflectionFinder $propertyReflectionFinder, + private bool $checkThisOnly, ) { - $this->ruleLevelHelper = $ruleLevelHelper; - $this->propertyDescriptor = $propertyDescriptor; - $this->propertyReflectionFinder = $propertyReflectionFinder; - $this->checkThisOnly = $checkThisOnly; } public function getNodeType(): string { - return \PhpParser\Node\Expr::class; + return Node\Expr::class; } public function processNode(Node $node, Scope $scope): array @@ -64,7 +54,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - /** @var \PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\StaticPropertyFetch $propertyFetch */ + /** @var Node\Expr\PropertyFetch|Node\Expr\StaticPropertyFetch $propertyFetch */ $propertyFetch = $node->var; $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($propertyFetch, $scope); if ($propertyReflection === null) { @@ -81,7 +71,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message(sprintf( '%s is not writable.', - $propertyDescription + $propertyDescription, ))->build(), ]; } diff --git a/src/Rules/Regexp/RegularExpressionPatternRule.php b/src/Rules/Regexp/RegularExpressionPatternRule.php index b1548f40b7..124d75a8b5 100644 --- a/src/Rules/Regexp/RegularExpressionPatternRule.php +++ b/src/Rules/Regexp/RegularExpressionPatternRule.php @@ -2,17 +2,24 @@ namespace PHPStan\Rules\Regexp; +use Nette\Utils\RegexpException; +use Nette\Utils\Strings; use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\TypeUtils; +use function in_array; +use function sprintf; +use function str_starts_with; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\FuncCall> + * @implements Rule */ -class RegularExpressionPatternRule implements \PHPStan\Rules\Rule +class RegularExpressionPatternRule implements Rule { public function getNodeType(): string @@ -38,8 +45,6 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param FuncCall $functionCall - * @param Scope $scope * @return string[] */ private function extractPatterns(FuncCall $functionCall, Scope $scope): array @@ -48,14 +53,14 @@ private function extractPatterns(FuncCall $functionCall, Scope $scope): array return []; } $functionName = strtolower((string) $functionCall->name); - if (!\Nette\Utils\Strings::startsWith($functionName, 'preg_')) { + if (!str_starts_with($functionName, 'preg_')) { return []; } - if (!isset($functionCall->args[0])) { + if (!isset($functionCall->getArgs()[0])) { return []; } - $patternNode = $functionCall->args[0]->value; + $patternNode = $functionCall->getArgs()[0]->value; $patternType = $scope->getType($patternNode); $patternStrings = []; @@ -114,8 +119,8 @@ private function extractPatterns(FuncCall $functionCall, Scope $scope): array private function validatePattern(string $pattern): ?string { try { - \Nette\Utils\Strings::match('', $pattern); - } catch (\Nette\Utils\RegexpException $e) { + Strings::match('', $pattern); + } catch (RegexpException $e) { return $e->getMessage(); } diff --git a/src/Rules/Registry.php b/src/Rules/Registry.php index fe75ac79f4..c13d06d02c 100644 --- a/src/Rules/Registry.php +++ b/src/Rules/Registry.php @@ -2,17 +2,21 @@ namespace PHPStan\Rules; +use PhpParser\Node; +use function class_implements; +use function class_parents; + class Registry { - /** @var \PHPStan\Rules\Rule[][] */ + /** @var Rule[][] */ private array $rules = []; - /** @var \PHPStan\Rules\Rule[][] */ + /** @var Rule[][] */ private array $cache = []; /** - * @param \PHPStan\Rules\Rule[] $rules + * @param Rule[] $rules */ public function __construct(array $rules) { @@ -22,11 +26,11 @@ public function __construct(array $rules) } /** - * @template TNodeType of \PhpParser\Node + * @template TNodeType of Node * @phpstan-param class-string $nodeType - * @param \PhpParser\Node $nodeType - * @phpstan-return array<\PHPStan\Rules\Rule> - * @return \PHPStan\Rules\Rule[] + * @param Node $nodeType + * @phpstan-return array> + * @return Rule[] */ public function getRules(string $nodeType): array { @@ -44,8 +48,8 @@ public function getRules(string $nodeType): array } /** - * @phpstan-var array<\PHPStan\Rules\Rule> $selectedRules - * @var \PHPStan\Rules\Rule[] $selectedRules + * @phpstan-var array> $selectedRules + * @var Rule[] $selectedRules */ $selectedRules = $this->cache[$nodeType]; diff --git a/src/Rules/RegistryFactory.php b/src/Rules/RegistryFactory.php index a632f11c6b..ebf00801d1 100644 --- a/src/Rules/RegistryFactory.php +++ b/src/Rules/RegistryFactory.php @@ -9,17 +9,14 @@ class RegistryFactory public const RULE_TAG = 'phpstan.rules.rule'; - private Container $container; - - public function __construct(Container $container) + public function __construct(private Container $container) { - $this->container = $container; } public function create(): Registry { return new Registry( - $this->container->getServicesByTag(self::RULE_TAG) + $this->container->getServicesByTag(self::RULE_TAG), ); } diff --git a/src/Rules/Rule.php b/src/Rules/Rule.php index 8ca22854e0..8f351cc4e9 100644 --- a/src/Rules/Rule.php +++ b/src/Rules/Rule.php @@ -7,21 +7,18 @@ /** * @api - * @phpstan-template TNodeType of \PhpParser\Node + * @phpstan-template TNodeType of Node */ interface Rule { /** * @phpstan-return class-string - * @return string */ public function getNodeType(): string; /** * @phpstan-param TNodeType $node - * @param \PhpParser\Node $node - * @param \PHPStan\Analyser\Scope $scope * @return (string|RuleError)[] errors */ public function processNode(Node $node, Scope $scope): array; diff --git a/src/Rules/RuleErrorBuilder.php b/src/Rules/RuleErrorBuilder.php index e309449564..091ffbf9db 100644 --- a/src/Rules/RuleErrorBuilder.php +++ b/src/Rules/RuleErrorBuilder.php @@ -2,6 +2,10 @@ namespace PHPStan\Rules; +use PHPStan\ShouldNotHappenException; +use function class_exists; +use function sprintf; + /** @api */ class RuleErrorBuilder { @@ -141,7 +145,7 @@ public function build(): RuleError /** @var class-string $className */ $className = sprintf('PHPStan\\Rules\\RuleErrors\\RuleError%d', $this->type); if (!class_exists($className)) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Class %s does not exist.', $className)); + throw new ShouldNotHappenException(sprintf('Class %s does not exist.', $className)); } $ruleError = new $className(); diff --git a/src/Rules/RuleErrors/RuleError1.php b/src/Rules/RuleErrors/RuleError1.php index 8b24e289f8..ef7771dea3 100644 --- a/src/Rules/RuleErrors/RuleError1.php +++ b/src/Rules/RuleErrors/RuleError1.php @@ -2,10 +2,12 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError1 implements \PHPStan\Rules\RuleError +class RuleError1 implements RuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError101.php b/src/Rules/RuleErrors/RuleError101.php index fe947a8d21..a4c08ae140 100644 --- a/src/Rules/RuleErrors/RuleError101.php +++ b/src/Rules/RuleErrors/RuleError101.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError101 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError101 implements RuleError, FileRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError103.php b/src/Rules/RuleErrors/RuleError103.php index 6303a368cc..04c4ae5083 100644 --- a/src/Rules/RuleErrors/RuleError103.php +++ b/src/Rules/RuleErrors/RuleError103.php @@ -2,10 +2,16 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError103 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError103 implements RuleError, LineRuleError, FileRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError105.php b/src/Rules/RuleErrors/RuleError105.php index 2f2f3077ea..0fb7b8bc41 100644 --- a/src/Rules/RuleErrors/RuleError105.php +++ b/src/Rules/RuleErrors/RuleError105.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError105 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError105 implements RuleError, TipRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError107.php b/src/Rules/RuleErrors/RuleError107.php index cfe2a83969..35b081c092 100644 --- a/src/Rules/RuleErrors/RuleError107.php +++ b/src/Rules/RuleErrors/RuleError107.php @@ -2,10 +2,16 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError107 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError107 implements RuleError, LineRuleError, TipRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError109.php b/src/Rules/RuleErrors/RuleError109.php index 5f4765bf2b..64d81d29db 100644 --- a/src/Rules/RuleErrors/RuleError109.php +++ b/src/Rules/RuleErrors/RuleError109.php @@ -2,10 +2,16 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError109 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError109 implements RuleError, FileRuleError, TipRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError11.php b/src/Rules/RuleErrors/RuleError11.php index 6603af96dd..50d8bdb997 100644 --- a/src/Rules/RuleErrors/RuleError11.php +++ b/src/Rules/RuleErrors/RuleError11.php @@ -2,10 +2,14 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError11 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\TipRuleError +class RuleError11 implements RuleError, LineRuleError, TipRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError111.php b/src/Rules/RuleErrors/RuleError111.php index a3b958496f..f3ad512dbc 100644 --- a/src/Rules/RuleErrors/RuleError111.php +++ b/src/Rules/RuleErrors/RuleError111.php @@ -2,10 +2,17 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError111 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError111 implements RuleError, LineRuleError, FileRuleError, TipRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError113.php b/src/Rules/RuleErrors/RuleError113.php index 1e770a3bbc..ee74c1f0ac 100644 --- a/src/Rules/RuleErrors/RuleError113.php +++ b/src/Rules/RuleErrors/RuleError113.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError113 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError113 implements RuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError115.php b/src/Rules/RuleErrors/RuleError115.php index 7f4a7c4853..7b5f1af9e5 100644 --- a/src/Rules/RuleErrors/RuleError115.php +++ b/src/Rules/RuleErrors/RuleError115.php @@ -2,10 +2,16 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError115 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError115 implements RuleError, LineRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError117.php b/src/Rules/RuleErrors/RuleError117.php index 2cffaad58b..d46b42784f 100644 --- a/src/Rules/RuleErrors/RuleError117.php +++ b/src/Rules/RuleErrors/RuleError117.php @@ -2,10 +2,16 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError117 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError117 implements RuleError, FileRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError119.php b/src/Rules/RuleErrors/RuleError119.php index e9bddceb6e..67ed0183e6 100644 --- a/src/Rules/RuleErrors/RuleError119.php +++ b/src/Rules/RuleErrors/RuleError119.php @@ -2,10 +2,17 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError119 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError119 implements RuleError, LineRuleError, FileRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError121.php b/src/Rules/RuleErrors/RuleError121.php index 1e26fb8da8..2c8995a0b3 100644 --- a/src/Rules/RuleErrors/RuleError121.php +++ b/src/Rules/RuleErrors/RuleError121.php @@ -2,10 +2,16 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError121 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError121 implements RuleError, TipRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError123.php b/src/Rules/RuleErrors/RuleError123.php index 98d76c5ea4..fedd8de5d9 100644 --- a/src/Rules/RuleErrors/RuleError123.php +++ b/src/Rules/RuleErrors/RuleError123.php @@ -2,10 +2,17 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError123 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError123 implements RuleError, LineRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError125.php b/src/Rules/RuleErrors/RuleError125.php index 1e1a3b3c6b..b8d1602fef 100644 --- a/src/Rules/RuleErrors/RuleError125.php +++ b/src/Rules/RuleErrors/RuleError125.php @@ -2,10 +2,17 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError125 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError125 implements RuleError, FileRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError127.php b/src/Rules/RuleErrors/RuleError127.php index dd268dda70..a32ffdb849 100644 --- a/src/Rules/RuleErrors/RuleError127.php +++ b/src/Rules/RuleErrors/RuleError127.php @@ -2,10 +2,18 @@ namespace PHPStan\Rules\RuleErrors; +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; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError127 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError127 implements RuleError, LineRuleError, FileRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError13.php b/src/Rules/RuleErrors/RuleError13.php index 91e53ebf45..51568e83da 100644 --- a/src/Rules/RuleErrors/RuleError13.php +++ b/src/Rules/RuleErrors/RuleError13.php @@ -2,10 +2,14 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError13 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError +class RuleError13 implements RuleError, FileRuleError, TipRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError15.php b/src/Rules/RuleErrors/RuleError15.php index ae65a55526..9c57396605 100644 --- a/src/Rules/RuleErrors/RuleError15.php +++ b/src/Rules/RuleErrors/RuleError15.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError15 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError +class RuleError15 implements RuleError, LineRuleError, FileRuleError, TipRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError17.php b/src/Rules/RuleErrors/RuleError17.php index a436eb2397..827f6a8724 100644 --- a/src/Rules/RuleErrors/RuleError17.php +++ b/src/Rules/RuleErrors/RuleError17.php @@ -2,10 +2,13 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError17 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\IdentifierRuleError +class RuleError17 implements RuleError, IdentifierRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError19.php b/src/Rules/RuleErrors/RuleError19.php index e5248ff1e9..3732da9802 100644 --- a/src/Rules/RuleErrors/RuleError19.php +++ b/src/Rules/RuleErrors/RuleError19.php @@ -2,10 +2,14 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError19 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\IdentifierRuleError +class RuleError19 implements RuleError, LineRuleError, IdentifierRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError21.php b/src/Rules/RuleErrors/RuleError21.php index 17d6cc978f..b1bb82ca7a 100644 --- a/src/Rules/RuleErrors/RuleError21.php +++ b/src/Rules/RuleErrors/RuleError21.php @@ -2,10 +2,14 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError21 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\IdentifierRuleError +class RuleError21 implements RuleError, FileRuleError, IdentifierRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError23.php b/src/Rules/RuleErrors/RuleError23.php index da249a89cf..1aa68678de 100644 --- a/src/Rules/RuleErrors/RuleError23.php +++ b/src/Rules/RuleErrors/RuleError23.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError23 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\IdentifierRuleError +class RuleError23 implements RuleError, LineRuleError, FileRuleError, IdentifierRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError25.php b/src/Rules/RuleErrors/RuleError25.php index 879216abd9..1c5c6001e8 100644 --- a/src/Rules/RuleErrors/RuleError25.php +++ b/src/Rules/RuleErrors/RuleError25.php @@ -2,10 +2,14 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError25 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError +class RuleError25 implements RuleError, TipRuleError, IdentifierRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError27.php b/src/Rules/RuleErrors/RuleError27.php index af88436146..e592c0d98e 100644 --- a/src/Rules/RuleErrors/RuleError27.php +++ b/src/Rules/RuleErrors/RuleError27.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError27 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError +class RuleError27 implements RuleError, LineRuleError, TipRuleError, IdentifierRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError29.php b/src/Rules/RuleErrors/RuleError29.php index 40855a0417..9f10ef1f20 100644 --- a/src/Rules/RuleErrors/RuleError29.php +++ b/src/Rules/RuleErrors/RuleError29.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError29 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError +class RuleError29 implements RuleError, FileRuleError, TipRuleError, IdentifierRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError3.php b/src/Rules/RuleErrors/RuleError3.php index 26169a3398..ce5c8fffcc 100644 --- a/src/Rules/RuleErrors/RuleError3.php +++ b/src/Rules/RuleErrors/RuleError3.php @@ -2,10 +2,13 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError3 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError +class RuleError3 implements RuleError, LineRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError31.php b/src/Rules/RuleErrors/RuleError31.php index 0495d84a4e..2df2e100c5 100644 --- a/src/Rules/RuleErrors/RuleError31.php +++ b/src/Rules/RuleErrors/RuleError31.php @@ -2,10 +2,16 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError31 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError +class RuleError31 implements RuleError, LineRuleError, FileRuleError, TipRuleError, IdentifierRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError33.php b/src/Rules/RuleErrors/RuleError33.php index 56198b54d7..0f37cede7c 100644 --- a/src/Rules/RuleErrors/RuleError33.php +++ b/src/Rules/RuleErrors/RuleError33.php @@ -2,10 +2,13 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError33 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\MetadataRuleError +class RuleError33 implements RuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError35.php b/src/Rules/RuleErrors/RuleError35.php index 9911746c12..65868071a8 100644 --- a/src/Rules/RuleErrors/RuleError35.php +++ b/src/Rules/RuleErrors/RuleError35.php @@ -2,10 +2,14 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError35 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\MetadataRuleError +class RuleError35 implements RuleError, LineRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError37.php b/src/Rules/RuleErrors/RuleError37.php index 119a3d5e87..ae5adf983c 100644 --- a/src/Rules/RuleErrors/RuleError37.php +++ b/src/Rules/RuleErrors/RuleError37.php @@ -2,10 +2,14 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError37 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\MetadataRuleError +class RuleError37 implements RuleError, FileRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError39.php b/src/Rules/RuleErrors/RuleError39.php index 56f98f8a05..a3699d7c68 100644 --- a/src/Rules/RuleErrors/RuleError39.php +++ b/src/Rules/RuleErrors/RuleError39.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError39 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\MetadataRuleError +class RuleError39 implements RuleError, LineRuleError, FileRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError41.php b/src/Rules/RuleErrors/RuleError41.php index 45e916e381..528a20c731 100644 --- a/src/Rules/RuleErrors/RuleError41.php +++ b/src/Rules/RuleErrors/RuleError41.php @@ -2,10 +2,14 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError41 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\MetadataRuleError +class RuleError41 implements RuleError, TipRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError43.php b/src/Rules/RuleErrors/RuleError43.php index 62824431f8..9992c86c9d 100644 --- a/src/Rules/RuleErrors/RuleError43.php +++ b/src/Rules/RuleErrors/RuleError43.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError43 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\MetadataRuleError +class RuleError43 implements RuleError, LineRuleError, TipRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError45.php b/src/Rules/RuleErrors/RuleError45.php index 254e17df77..b81bfcd3b7 100644 --- a/src/Rules/RuleErrors/RuleError45.php +++ b/src/Rules/RuleErrors/RuleError45.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError45 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\MetadataRuleError +class RuleError45 implements RuleError, FileRuleError, TipRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError47.php b/src/Rules/RuleErrors/RuleError47.php index 9a17976afd..d4f0af6e2a 100644 --- a/src/Rules/RuleErrors/RuleError47.php +++ b/src/Rules/RuleErrors/RuleError47.php @@ -2,10 +2,16 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError47 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\MetadataRuleError +class RuleError47 implements RuleError, LineRuleError, FileRuleError, TipRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError49.php b/src/Rules/RuleErrors/RuleError49.php index 6780a90cde..6b8aa73a5b 100644 --- a/src/Rules/RuleErrors/RuleError49.php +++ b/src/Rules/RuleErrors/RuleError49.php @@ -2,10 +2,14 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError49 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError +class RuleError49 implements RuleError, IdentifierRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError5.php b/src/Rules/RuleErrors/RuleError5.php index 99f66f59f4..58f47a9053 100644 --- a/src/Rules/RuleErrors/RuleError5.php +++ b/src/Rules/RuleErrors/RuleError5.php @@ -2,10 +2,13 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError5 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError +class RuleError5 implements RuleError, FileRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError51.php b/src/Rules/RuleErrors/RuleError51.php index 7c70ad7c0b..a008143cd2 100644 --- a/src/Rules/RuleErrors/RuleError51.php +++ b/src/Rules/RuleErrors/RuleError51.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError51 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError +class RuleError51 implements RuleError, LineRuleError, IdentifierRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError53.php b/src/Rules/RuleErrors/RuleError53.php index 23a1bad346..e11bdaf0cb 100644 --- a/src/Rules/RuleErrors/RuleError53.php +++ b/src/Rules/RuleErrors/RuleError53.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError53 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError +class RuleError53 implements RuleError, FileRuleError, IdentifierRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError55.php b/src/Rules/RuleErrors/RuleError55.php index b0fe074d84..3b9aefc997 100644 --- a/src/Rules/RuleErrors/RuleError55.php +++ b/src/Rules/RuleErrors/RuleError55.php @@ -2,10 +2,16 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError55 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError +class RuleError55 implements RuleError, LineRuleError, FileRuleError, IdentifierRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError57.php b/src/Rules/RuleErrors/RuleError57.php index 5395e55b8d..4fabd64eac 100644 --- a/src/Rules/RuleErrors/RuleError57.php +++ b/src/Rules/RuleErrors/RuleError57.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError57 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError +class RuleError57 implements RuleError, TipRuleError, IdentifierRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError59.php b/src/Rules/RuleErrors/RuleError59.php index db750c988e..837c62602d 100644 --- a/src/Rules/RuleErrors/RuleError59.php +++ b/src/Rules/RuleErrors/RuleError59.php @@ -2,10 +2,16 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError59 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError +class RuleError59 implements RuleError, LineRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError61.php b/src/Rules/RuleErrors/RuleError61.php index 63286dd5b1..a263eafb22 100644 --- a/src/Rules/RuleErrors/RuleError61.php +++ b/src/Rules/RuleErrors/RuleError61.php @@ -2,10 +2,16 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError61 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError +class RuleError61 implements RuleError, FileRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError63.php b/src/Rules/RuleErrors/RuleError63.php index dcfe4ac0ee..80a680ec0d 100644 --- a/src/Rules/RuleErrors/RuleError63.php +++ b/src/Rules/RuleErrors/RuleError63.php @@ -2,10 +2,17 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError63 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\MetadataRuleError +class RuleError63 implements RuleError, LineRuleError, FileRuleError, TipRuleError, IdentifierRuleError, MetadataRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError65.php b/src/Rules/RuleErrors/RuleError65.php index c37e5c66ca..095f49475d 100644 --- a/src/Rules/RuleErrors/RuleError65.php +++ b/src/Rules/RuleErrors/RuleError65.php @@ -2,10 +2,13 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError65 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError65 implements RuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError67.php b/src/Rules/RuleErrors/RuleError67.php index 924b6b70f8..08a214f997 100644 --- a/src/Rules/RuleErrors/RuleError67.php +++ b/src/Rules/RuleErrors/RuleError67.php @@ -2,10 +2,14 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError67 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError67 implements RuleError, LineRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError69.php b/src/Rules/RuleErrors/RuleError69.php index 3cb5843e4e..afa9983d5d 100644 --- a/src/Rules/RuleErrors/RuleError69.php +++ b/src/Rules/RuleErrors/RuleError69.php @@ -2,10 +2,14 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError69 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError69 implements RuleError, FileRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError7.php b/src/Rules/RuleErrors/RuleError7.php index 606e57ecb4..4ce7c6b45e 100644 --- a/src/Rules/RuleErrors/RuleError7.php +++ b/src/Rules/RuleErrors/RuleError7.php @@ -2,10 +2,14 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError7 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError +class RuleError7 implements RuleError, LineRuleError, FileRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError71.php b/src/Rules/RuleErrors/RuleError71.php index 27c04fc061..06db73390a 100644 --- a/src/Rules/RuleErrors/RuleError71.php +++ b/src/Rules/RuleErrors/RuleError71.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError71 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError71 implements RuleError, LineRuleError, FileRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError73.php b/src/Rules/RuleErrors/RuleError73.php index 55d9a91539..8cdaeaa36d 100644 --- a/src/Rules/RuleErrors/RuleError73.php +++ b/src/Rules/RuleErrors/RuleError73.php @@ -2,10 +2,14 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError73 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError73 implements RuleError, TipRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError75.php b/src/Rules/RuleErrors/RuleError75.php index 600407fbf1..3195db7454 100644 --- a/src/Rules/RuleErrors/RuleError75.php +++ b/src/Rules/RuleErrors/RuleError75.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError75 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError75 implements RuleError, LineRuleError, TipRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError77.php b/src/Rules/RuleErrors/RuleError77.php index 56512c8faf..12a34ab040 100644 --- a/src/Rules/RuleErrors/RuleError77.php +++ b/src/Rules/RuleErrors/RuleError77.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError77 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError77 implements RuleError, FileRuleError, TipRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError79.php b/src/Rules/RuleErrors/RuleError79.php index 95a10f354f..ac103ea3f5 100644 --- a/src/Rules/RuleErrors/RuleError79.php +++ b/src/Rules/RuleErrors/RuleError79.php @@ -2,10 +2,16 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError79 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError79 implements RuleError, LineRuleError, FileRuleError, TipRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError81.php b/src/Rules/RuleErrors/RuleError81.php index 122bd698e1..1412335a8a 100644 --- a/src/Rules/RuleErrors/RuleError81.php +++ b/src/Rules/RuleErrors/RuleError81.php @@ -2,10 +2,14 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError81 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError81 implements RuleError, IdentifierRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError83.php b/src/Rules/RuleErrors/RuleError83.php index 3e7248cad3..ceb2141456 100644 --- a/src/Rules/RuleErrors/RuleError83.php +++ b/src/Rules/RuleErrors/RuleError83.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError83 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError83 implements RuleError, LineRuleError, IdentifierRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError85.php b/src/Rules/RuleErrors/RuleError85.php index 68866e7a18..48a2c071f1 100644 --- a/src/Rules/RuleErrors/RuleError85.php +++ b/src/Rules/RuleErrors/RuleError85.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError85 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError85 implements RuleError, FileRuleError, IdentifierRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError87.php b/src/Rules/RuleErrors/RuleError87.php index 742bb0c7b2..04ccf29f73 100644 --- a/src/Rules/RuleErrors/RuleError87.php +++ b/src/Rules/RuleErrors/RuleError87.php @@ -2,10 +2,16 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError87 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError87 implements RuleError, LineRuleError, FileRuleError, IdentifierRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError89.php b/src/Rules/RuleErrors/RuleError89.php index 83f5a00039..3b207cf2ad 100644 --- a/src/Rules/RuleErrors/RuleError89.php +++ b/src/Rules/RuleErrors/RuleError89.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError89 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError89 implements RuleError, TipRuleError, IdentifierRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError9.php b/src/Rules/RuleErrors/RuleError9.php index a4cd956791..e93934d20b 100644 --- a/src/Rules/RuleErrors/RuleError9.php +++ b/src/Rules/RuleErrors/RuleError9.php @@ -2,10 +2,13 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError9 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\TipRuleError +class RuleError9 implements RuleError, TipRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError91.php b/src/Rules/RuleErrors/RuleError91.php index dfd25a1e71..a0fc78df5a 100644 --- a/src/Rules/RuleErrors/RuleError91.php +++ b/src/Rules/RuleErrors/RuleError91.php @@ -2,10 +2,16 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError91 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError91 implements RuleError, LineRuleError, TipRuleError, IdentifierRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError93.php b/src/Rules/RuleErrors/RuleError93.php index 691a43d24a..a9cdb69c6e 100644 --- a/src/Rules/RuleErrors/RuleError93.php +++ b/src/Rules/RuleErrors/RuleError93.php @@ -2,10 +2,16 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError93 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError93 implements RuleError, FileRuleError, TipRuleError, IdentifierRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError95.php b/src/Rules/RuleErrors/RuleError95.php index b3170a3515..117c4b574d 100644 --- a/src/Rules/RuleErrors/RuleError95.php +++ b/src/Rules/RuleErrors/RuleError95.php @@ -2,10 +2,17 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\FileRuleError; +use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; +use PHPStan\Rules\TipRuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError95 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\FileRuleError, \PHPStan\Rules\TipRuleError, \PHPStan\Rules\IdentifierRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError95 implements RuleError, LineRuleError, FileRuleError, TipRuleError, IdentifierRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError97.php b/src/Rules/RuleErrors/RuleError97.php index 1a7c512453..da07902233 100644 --- a/src/Rules/RuleErrors/RuleError97.php +++ b/src/Rules/RuleErrors/RuleError97.php @@ -2,10 +2,14 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError97 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError97 implements RuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleErrors/RuleError99.php b/src/Rules/RuleErrors/RuleError99.php index 76064d3486..b26457a1e1 100644 --- a/src/Rules/RuleErrors/RuleError99.php +++ b/src/Rules/RuleErrors/RuleError99.php @@ -2,10 +2,15 @@ namespace PHPStan\Rules\RuleErrors; +use PHPStan\Rules\LineRuleError; +use PHPStan\Rules\MetadataRuleError; +use PHPStan\Rules\NonIgnorableRuleError; +use PHPStan\Rules\RuleError; + /** * @internal Use PHPStan\Rules\RuleErrorBuilder instead. */ -class RuleError99 implements \PHPStan\Rules\RuleError, \PHPStan\Rules\LineRuleError, \PHPStan\Rules\MetadataRuleError, \PHPStan\Rules\NonIgnorableRuleError +class RuleError99 implements RuleError, LineRuleError, MetadataRuleError, NonIgnorableRuleError { public string $message; diff --git a/src/Rules/RuleLevelHelper.php b/src/Rules/RuleLevelHelper.php index 240258921c..bc1b80ebe4 100644 --- a/src/Rules/RuleLevelHelper.php +++ b/src/Rules/RuleLevelHelper.php @@ -3,7 +3,6 @@ namespace PHPStan\Rules; use PhpParser\Node\Expr; -use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\BenevolentUnionType; @@ -18,35 +17,25 @@ use PHPStan\Type\StrictMixedType; 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 count; +use function sprintf; +use function strpos; class RuleLevelHelper { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - private bool $checkNullables; - - private bool $checkThisOnly; - - private bool $checkUnionTypes; - - private bool $checkExplicitMixed; - public function __construct( - ReflectionProvider $reflectionProvider, - bool $checkNullables, - bool $checkThisOnly, - bool $checkUnionTypes, - bool $checkExplicitMixed = false + private ReflectionProvider $reflectionProvider, + private bool $checkNullables, + private bool $checkThisOnly, + private bool $checkUnionTypes, + private bool $checkExplicitMixed, ) { - $this->reflectionProvider = $reflectionProvider; - $this->checkNullables = $checkNullables; - $this->checkThisOnly = $checkThisOnly; - $this->checkUnionTypes = $checkUnionTypes; - $this->checkExplicitMixed = $checkExplicitMixed; } /** @api */ @@ -60,10 +49,22 @@ public function accepts(Type $acceptingType, Type $acceptedType, bool $strictTyp { if ( $this->checkExplicitMixed - && $acceptedType instanceof MixedType - && $acceptedType->isExplicitMixed() ) { - $acceptedType = new StrictMixedType(); + $traverse = static function (Type $type, callable $traverse): Type { + if ($type instanceof TemplateMixedType) { + return $type->toStrictMixedType(); + } + if ( + $type instanceof MixedType + && $type->isExplicitMixed() + ) { + return new StrictMixedType(); + } + + return $traverse($type); + }; + $acceptingType = TypeTraverser::map($acceptingType, $traverse); + $acceptedType = TypeTraverser::map($acceptedType, $traverse); } if ( @@ -75,7 +76,8 @@ public function accepts(Type $acceptingType, Type $acceptedType, bool $strictTyp $acceptedType = TypeCombinator::removeNull($acceptedType); } - if ($acceptingType instanceof UnionType && !$acceptedType instanceof CompoundType) { + $accepts = $acceptingType->accepts($acceptedType, $strictTypes); + if (!$accepts->yes() && $acceptingType instanceof UnionType && !$acceptedType instanceof CompoundType) { foreach ($acceptingType->getTypes() as $innerType) { if (self::accepts($innerType, $acceptedType, $strictTypes)) { return true; @@ -95,44 +97,34 @@ public function accepts(Type $acceptingType, Type $acceptedType, bool $strictTyp return self::accepts( $acceptingType->getIterableKeyType(), $acceptedType->getIterableKeyType(), - $strictTypes + $strictTypes, ) && self::accepts( $acceptingType->getIterableValueType(), $acceptedType->getIterableValueType(), - $strictTypes + $strictTypes, ); } - $accepts = $acceptingType->accepts($acceptedType, $strictTypes); - return $this->checkUnionTypes ? $accepts->yes() : !$accepts->no(); } /** * @api - * @param Scope $scope - * @param Expr $var - * @param string $unknownClassErrorPattern * @param callable(Type $type): bool $unionTypeCriteriaCallback - * @return FoundTypeResult */ public function findTypeToCheck( Scope $scope, Expr $var, string $unknownClassErrorPattern, - callable $unionTypeCriteriaCallback + callable $unionTypeCriteriaCallback, ): FoundTypeResult { if ($this->checkThisOnly && !$this->isThis($var)) { - return new FoundTypeResult(new ErrorType(), [], []); + return new FoundTypeResult(new ErrorType(), [], [], null); } $type = $scope->getType($var); if (!$this->checkNullables && !$type instanceof NullType) { - $type = \PHPStan\Type\TypeCombinator::removeNull($type); - } - - if (TypeCombinator::containsNull($type)) { - $type = $scope->getType(NullsafeOperatorHelper::getNullsafeShortcircuitedExpr($var)); + $type = TypeCombinator::removeNull($type); } if ( @@ -141,11 +133,11 @@ public function findTypeToCheck( && !$type instanceof TemplateMixedType && $type->isExplicitMixed() ) { - return new FoundTypeResult(new StrictMixedType(), [], []); + return new FoundTypeResult(new StrictMixedType(), [], [], null); } if ($type instanceof MixedType || $type instanceof NeverType) { - return new FoundTypeResult(new ErrorType(), [], []); + return new FoundTypeResult(new ErrorType(), [], [], null); } if ($type instanceof StaticType) { $type = $type->getStaticObjectType(); @@ -171,12 +163,12 @@ public function findTypeToCheck( } if (count($errors) > 0 || $hasClassExistsClass) { - return new FoundTypeResult(new ErrorType(), [], $errors); + return new FoundTypeResult(new ErrorType(), [], $errors, null); } if (!$this->checkUnionTypes) { if ($type instanceof ObjectWithoutClassType) { - return new FoundTypeResult(new ErrorType(), [], []); + return new FoundTypeResult(new ErrorType(), [], [], null); } if ($type instanceof UnionType) { $newTypes = []; @@ -189,12 +181,17 @@ public function findTypeToCheck( } if (count($newTypes) > 0) { - return new FoundTypeResult(TypeCombinator::union(...$newTypes), $directClassNames, []); + return new FoundTypeResult(TypeCombinator::union(...$newTypes), $directClassNames, [], null); } } } - return new FoundTypeResult($type, $directClassNames, []); + $tip = null; + if (strpos($type->describe(VerbosityLevel::typeOnly()), 'PhpParser\\Node\\Arg|PhpParser\\Node\\VariadicPlaceholder') !== false && !$unionTypeCriteriaCallback($type)) { + $tip = 'Use ->getArgs() instead of ->args.'; + } + + return new FoundTypeResult($type, $directClassNames, [], $tip); } } diff --git a/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php index cf064a1c87..01b4e33160 100644 --- a/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRule.php @@ -10,6 +10,7 @@ use PHPStan\Type\NullType; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** * @implements Rule @@ -49,8 +50,8 @@ public function processNode(Node $node, Scope $scope): array } $messages[] = RuleErrorBuilder::message(sprintf( - 'Anonymous function never returns %s so it can be removed from the return typehint.', - $type->describe(VerbosityLevel::getRecommendedLevelByType($type)) + 'Anonymous function never returns %s so it can be removed from the return type.', + $type->describe(VerbosityLevel::getRecommendedLevelByType($type)), ))->build(); } diff --git a/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php index 0bcb7fa810..60cd322ce6 100644 --- a/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideClosureReturnTypehintRule.php @@ -11,9 +11,11 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function count; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\ClosureReturnStatementsNode> + * @implements Rule */ class TooWideClosureReturnTypehintRule implements Rule { @@ -71,8 +73,8 @@ public function processNode(Node $node, Scope $scope): array } $messages[] = RuleErrorBuilder::message(sprintf( - 'Anonymous function never returns %s so it can be removed from the return typehint.', - $type->describe(VerbosityLevel::getRecommendedLevelByType($type)) + 'Anonymous function never returns %s so it can be removed from the return type.', + $type->describe(VerbosityLevel::getRecommendedLevelByType($type)), ))->build(); } diff --git a/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php index 827d6d9490..35f4080685 100644 --- a/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRule.php @@ -9,12 +9,16 @@ use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; +use PHPStan\Type\NullType; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function count; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\FunctionReturnStatementsNode> + * @implements Rule */ class TooWideFunctionReturnTypehintRule implements Rule { @@ -28,7 +32,7 @@ public function processNode(Node $node, Scope $scope): array { $function = $scope->getFunction(); if (!$function instanceof FunctionReflection) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $functionReturnType = ParametersAcceptorSelector::selectSingle($function->getVariants())->getReturnType(); @@ -67,10 +71,20 @@ public function processNode(Node $node, Scope $scope): array continue; } + if ($type instanceof NullType && !$node->hasNativeReturnTypehint()) { + foreach ($node->getExecutionEnds() as $executionEnd) { + if ($executionEnd->getStatementResult()->isAlwaysTerminating()) { + continue; + } + + continue 2; + } + } + $messages[] = RuleErrorBuilder::message(sprintf( - 'Function %s() never returns %s so it can be removed from the return typehint.', + 'Function %s() never returns %s so it can be removed from the return type.', $function->getName(), - $type->describe(VerbosityLevel::getRecommendedLevelByType($type)) + $type->describe(VerbosityLevel::getRecommendedLevelByType($type)), ))->build(); } diff --git a/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php b/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php index 5b1210ff9b..259487e3bf 100644 --- a/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php +++ b/src/Rules/TooWideTypehints/TooWideMethodReturnTypehintRule.php @@ -9,23 +9,23 @@ use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\NullType; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function count; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PHPStan\Node\MethodReturnStatementsNode> + * @implements Rule */ class TooWideMethodReturnTypehintRule implements Rule { - private bool $checkProtectedAndPublicMethods; - - public function __construct(bool $checkProtectedAndPublicMethods) + public function __construct(private bool $checkProtectedAndPublicMethods) { - $this->checkProtectedAndPublicMethods = $checkProtectedAndPublicMethods; } public function getNodeType(): string @@ -37,7 +37,7 @@ public function processNode(Node $node, Scope $scope): array { $method = $scope->getFunction(); if (!$method instanceof MethodReflection) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $isFirstDeclaration = $method->getPrototype()->getDeclaringClass() === $method->getDeclaringClass(); if (!$method->isPrivate()) { @@ -92,11 +92,21 @@ public function processNode(Node $node, Scope $scope): array continue; } + if ($type instanceof NullType && !$node->hasNativeReturnTypehint()) { + foreach ($node->getExecutionEnds() as $executionEnd) { + if ($executionEnd->getStatementResult()->isAlwaysTerminating()) { + continue; + } + + continue 2; + } + } + $messages[] = RuleErrorBuilder::message(sprintf( - 'Method %s::%s() never returns %s so it can be removed from the return typehint.', + 'Method %s::%s() never returns %s so it can be removed from the return type.', $method->getDeclaringClass()->getDisplayName(), $method->getName(), - $type->describe(VerbosityLevel::getRecommendedLevelByType($type)) + $type->describe(VerbosityLevel::getRecommendedLevelByType($type)), ))->build(); } diff --git a/src/Rules/UnusedFunctionParametersCheck.php b/src/Rules/UnusedFunctionParametersCheck.php index 40233b2c47..890fc5d3f6 100644 --- a/src/Rules/UnusedFunctionParametersCheck.php +++ b/src/Rules/UnusedFunctionParametersCheck.php @@ -6,23 +6,23 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\Constant\ConstantStringType; +use function array_fill_keys; +use function array_keys; +use function array_merge; +use function is_array; +use function is_string; +use function sprintf; class UnusedFunctionParametersCheck { - private ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct(private ReflectionProvider $reflectionProvider) { - $this->reflectionProvider = $reflectionProvider; } /** - * @param \PHPStan\Analyser\Scope $scope * @param string[] $parameterNames - * @param \PhpParser\Node[] $statements - * @param string $unusedParameterMessage - * @param string $identifier + * @param Node[] $statements * @param mixed[] $additionalMetadata * @return RuleError[] */ @@ -32,7 +32,7 @@ public function getUnusedParameters( array $statements, string $unusedParameterMessage, string $identifier, - array $additionalMetadata + array $additionalMetadata, ): array { $unusedParameters = array_fill_keys($parameterNames, true); @@ -46,7 +46,7 @@ public function getUnusedParameters( $errors = []; foreach (array_keys($unusedParameters) as $name) { $errors[] = RuleErrorBuilder::message( - sprintf($unusedParameterMessage, $name) + sprintf($unusedParameterMessage, $name), )->identifier($identifier)->metadata($additionalMetadata + ['variableName' => $name])->build(); } @@ -54,8 +54,7 @@ public function getUnusedParameters( } /** - * @param \PHPStan\Analyser\Scope $scope - * @param \PhpParser\Node[]|\PhpParser\Node|scalar $node + * @param Node[]|Node|scalar $node * @return string[] */ private function getUsedVariables(Scope $scope, $node): array @@ -79,7 +78,7 @@ private function getUsedVariables(Scope $scope, $node): array && $node->name instanceof Node\Name && (string) $node->name === 'compact' ) { - foreach ($node->args as $arg) { + foreach ($node->getArgs() as $arg) { $argType = $scope->getType($arg->value); if (!($argType instanceof ConstantStringType)) { continue; diff --git a/src/Rules/Variables/CompactVariablesRule.php b/src/Rules/Variables/CompactVariablesRule.php index 6292893e6a..454082e900 100644 --- a/src/Rules/Variables/CompactVariablesRule.php +++ b/src/Rules/Variables/CompactVariablesRule.php @@ -9,18 +9,18 @@ use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Type; +use function array_merge; +use function sprintf; +use function strtolower; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\FuncCall> + * @implements Rule */ final class CompactVariablesRule implements Rule { - private bool $checkMaybeUndefinedVariables; - - public function __construct(bool $checkMaybeUndefinedVariables) + public function __construct(private bool $checkMaybeUndefinedVariables) { - $this->checkMaybeUndefinedVariables = $checkMaybeUndefinedVariables; } public function getNodeType(): string @@ -40,7 +40,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $functionArguments = $node->args; + $functionArguments = $node->getArgs(); $messages = []; foreach ($functionArguments as $argument) { @@ -52,11 +52,11 @@ public function processNode(Node $node, Scope $scope): array if ($scopeHasVariable->no()) { $messages[] = RuleErrorBuilder::message( - sprintf('Call to function compact() contains undefined variable $%s.', $variableName) + sprintf('Call to function compact() contains undefined variable $%s.', $variableName), )->line($argument->getLine())->build(); } elseif ($this->checkMaybeUndefinedVariables && $scopeHasVariable->maybe()) { $messages[] = RuleErrorBuilder::message( - sprintf('Call to function compact() contains possibly undefined variable $%s.', $variableName) + sprintf('Call to function compact() contains possibly undefined variable $%s.', $variableName), )->line($argument->getLine())->build(); } } @@ -66,7 +66,6 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param Type $type * @return array */ private function findConstantStrings(Type $type): array diff --git a/src/Rules/Variables/DefinedVariableRule.php b/src/Rules/Variables/DefinedVariableRule.php index f50735792d..9a69183eaa 100644 --- a/src/Rules/Variables/DefinedVariableRule.php +++ b/src/Rules/Variables/DefinedVariableRule.php @@ -5,25 +5,23 @@ use PhpParser\Node; use PhpParser\Node\Expr\Variable; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function in_array; +use function is_string; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Variable> + * @implements Rule */ -class DefinedVariableRule implements \PHPStan\Rules\Rule +class DefinedVariableRule implements Rule { - private bool $cliArgumentsVariablesRegistered; - - private bool $checkMaybeUndefinedVariables; - public function __construct( - bool $cliArgumentsVariablesRegistered, - bool $checkMaybeUndefinedVariables + private bool $cliArgumentsVariablesRegistered, + private bool $checkMaybeUndefinedVariables, ) { - $this->cliArgumentsVariablesRegistered = $cliArgumentsVariablesRegistered; - $this->checkMaybeUndefinedVariables = $checkMaybeUndefinedVariables; } public function getNodeType(): string @@ -90,7 +88,6 @@ public function processNode(Node $node, Scope $scope): array } /** - * @param Scope $scope * @return array> */ private function getParentVariables(Scope $scope): array diff --git a/src/Rules/Variables/EmptyRule.php b/src/Rules/Variables/EmptyRule.php new file mode 100644 index 0000000000..9a48592cb9 --- /dev/null +++ b/src/Rules/Variables/EmptyRule.php @@ -0,0 +1,69 @@ + + */ +class EmptyRule implements Rule +{ + + public function __construct(private IssetCheck $issetCheck) + { + } + + public function getNodeType(): string + { + return Node\Expr\Empty_::class; + } + + 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()); + if ($isNull->maybe()) { + return null; + } + if ($isFalsey->maybe()) { + return null; + } + + if ($isNull->yes()) { + if ($isFalsey->yes()) { + return 'is always falsy'; + } + if ($isFalsey->no()) { + return 'is not falsy'; + } + + return 'is always null'; + } + + if ($isFalsey->yes()) { + return 'is always falsy'; + } + + if ($isFalsey->no()) { + return 'is not falsy'; + } + + return 'is not nullable'; + }); + + if ($error === null) { + return []; + } + + return [$error]; + } + +} diff --git a/src/Rules/Variables/IssetRule.php b/src/Rules/Variables/IssetRule.php index 78ab199e97..55cd5dc05f 100644 --- a/src/Rules/Variables/IssetRule.php +++ b/src/Rules/Variables/IssetRule.php @@ -5,18 +5,18 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Rules\IssetCheck; +use PHPStan\Rules\Rule; +use PHPStan\Type\NullType; +use PHPStan\Type\Type; /** - * @implements \PHPStan\Rules\Rule + * @implements Rule */ -class IssetRule implements \PHPStan\Rules\Rule +class IssetRule implements Rule { - private IssetCheck $issetCheck; - - public function __construct(IssetCheck $issetCheck) + public function __construct(private IssetCheck $issetCheck) { - $this->issetCheck = $issetCheck; } public function getNodeType(): string @@ -28,7 +28,18 @@ public function processNode(Node $node, Scope $scope): array { $messages = []; foreach ($node->vars as $var) { - $error = $this->issetCheck->check($var, $scope, 'in isset()'); + $error = $this->issetCheck->check($var, $scope, 'in isset()', static function (Type $type): ?string { + $isNull = (new NullType())->isSuperTypeOf($type); + if ($isNull->maybe()) { + return null; + } + + if ($isNull->yes()) { + return 'is always null'; + } + + return 'is not nullable'; + }); if ($error === null) { continue; } diff --git a/src/Rules/Variables/NullCoalesceRule.php b/src/Rules/Variables/NullCoalesceRule.php index 5c6790e3f8..8666ab4f7e 100644 --- a/src/Rules/Variables/NullCoalesceRule.php +++ b/src/Rules/Variables/NullCoalesceRule.php @@ -5,31 +5,44 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Rules\IssetCheck; +use PHPStan\Rules\Rule; +use PHPStan\Type\NullType; +use PHPStan\Type\Type; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr> + * @implements Rule */ -class NullCoalesceRule implements \PHPStan\Rules\Rule +class NullCoalesceRule implements Rule { - private IssetCheck $issetCheck; - - public function __construct(IssetCheck $issetCheck) + public function __construct(private IssetCheck $issetCheck) { - $this->issetCheck = $issetCheck; } public function getNodeType(): string { - return \PhpParser\Node\Expr::class; + return Node\Expr::class; } public function processNode(Node $node, Scope $scope): array { + $typeMessageCallback = static function (Type $type): ?string { + $isNull = (new NullType())->isSuperTypeOf($type); + if ($isNull->maybe()) { + return null; + } + + if ($isNull->yes()) { + return 'is always null'; + } + + return 'is not nullable'; + }; + if ($node instanceof Node\Expr\BinaryOp\Coalesce) { - $error = $this->issetCheck->check($node->left, $scope, 'on left side of ??'); + $error = $this->issetCheck->check($node->left, $scope, 'on left side of ??', $typeMessageCallback); } elseif ($node instanceof Node\Expr\AssignOp\Coalesce) { - $error = $this->issetCheck->check($node->var, $scope, 'on left side of ??='); + $error = $this->issetCheck->check($node->var, $scope, 'on left side of ??=', $typeMessageCallback); } else { return []; } diff --git a/src/Rules/Variables/ThrowTypeRule.php b/src/Rules/Variables/ThrowTypeRule.php index 51b4c0bbfe..deb7a22856 100644 --- a/src/Rules/Variables/ThrowTypeRule.php +++ b/src/Rules/Variables/ThrowTypeRule.php @@ -4,43 +4,41 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use Throwable; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Throw_> + * @implements Rule */ -class ThrowTypeRule implements \PHPStan\Rules\Rule +class ThrowTypeRule implements Rule { - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - public function __construct( - RuleLevelHelper $ruleLevelHelper + private RuleLevelHelper $ruleLevelHelper, ) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string { - return \PhpParser\Node\Stmt\Throw_::class; + return Node\Stmt\Throw_::class; } public function processNode(Node $node, Scope $scope): array { - $throwableType = new ObjectType(\Throwable::class); + $throwableType = new ObjectType(Throwable::class); $typeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, $node->expr, 'Throwing object of an unknown class %s.', - static function (Type $type) use ($throwableType): bool { - return $throwableType->isSuperTypeOf($type)->yes(); - } + static fn (Type $type): bool => $throwableType->isSuperTypeOf($type)->yes(), ); $foundType = $typeResult->getType(); @@ -56,7 +54,7 @@ static function (Type $type) use ($throwableType): bool { return [ RuleErrorBuilder::message(sprintf( 'Invalid type %s to throw.', - $foundType->describe(VerbosityLevel::typeOnly()) + $foundType->describe(VerbosityLevel::typeOnly()), ))->build(), ]; } diff --git a/src/Rules/Variables/UnsetRule.php b/src/Rules/Variables/UnsetRule.php index 58ab8457d8..2484802dc1 100644 --- a/src/Rules/Variables/UnsetRule.php +++ b/src/Rules/Variables/UnsetRule.php @@ -4,14 +4,17 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\VerbosityLevel; +use function is_string; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Stmt\Unset_> + * @implements Rule */ -class UnsetRule implements \PHPStan\Rules\Rule +class UnsetRule implements Rule { public function getNodeType(): string @@ -42,7 +45,7 @@ private function canBeUnset(Node $node, Scope $scope): ?RuleError $hasVariable = $scope->hasVariableType($node->name); if ($hasVariable->no()) { return RuleErrorBuilder::message( - sprintf('Call to function unset() contains undefined variable $%s.', $node->name) + sprintf('Call to function unset() contains undefined variable $%s.', $node->name), )->line($node->getLine())->build(); } } elseif ($node instanceof Node\Expr\ArrayDimFetch && $node->dim !== null) { @@ -54,8 +57,8 @@ private function canBeUnset(Node $node, Scope $scope): ?RuleError sprintf( 'Cannot unset offset %s on %s.', $dimType->describe(VerbosityLevel::value()), - $type->describe(VerbosityLevel::value()) - ) + $type->describe(VerbosityLevel::value()), + ), )->line($node->getLine())->build(); } diff --git a/src/Rules/Variables/VariableCertaintyInIssetRule.php b/src/Rules/Variables/VariableCertaintyInIssetRule.php deleted file mode 100644 index 242951798d..0000000000 --- a/src/Rules/Variables/VariableCertaintyInIssetRule.php +++ /dev/null @@ -1,71 +0,0 @@ - - */ -class VariableCertaintyInIssetRule implements \PHPStan\Rules\Rule -{ - - public function getNodeType(): string - { - return Node\Expr\Isset_::class; - } - - public function processNode(Node $node, Scope $scope): array - { - $messages = []; - foreach ($node->vars as $var) { - $isSubNode = false; - while ( - $var instanceof Node\Expr\ArrayDimFetch - || $var instanceof Node\Expr\PropertyFetch - || ( - $var instanceof Node\Expr\StaticPropertyFetch - && $var->class instanceof Node\Expr - ) - ) { - if ($var instanceof Node\Expr\StaticPropertyFetch) { - $var = $var->class; - } else { - $var = $var->var; - } - $isSubNode = true; - } - - if (!$var instanceof Node\Expr\Variable || !is_string($var->name) || $var->name === '_SESSION') { - continue; - } - - $certainty = $scope->hasVariableType($var->name); - if ($certainty->no()) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Variable $%s in isset() is never defined.', - $var->name - ))->build(); - } elseif ($certainty->yes() && !$isSubNode) { - $variableType = $scope->getVariableType($var->name); - if ($variableType->isSuperTypeOf(new NullType())->no()) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Variable $%s in isset() always exists and is not nullable.', - $var->name - ))->build(); - } elseif ((new NullType())->isSuperTypeOf($variableType)->yes()) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Variable $%s in isset() is always null.', - $var->name - ))->build(); - } - } - } - - return $messages; - } - -} diff --git a/src/Rules/Variables/VariableCertaintyNullCoalesceRule.php b/src/Rules/Variables/VariableCertaintyNullCoalesceRule.php deleted file mode 100644 index 5a8b9e1323..0000000000 --- a/src/Rules/Variables/VariableCertaintyNullCoalesceRule.php +++ /dev/null @@ -1,81 +0,0 @@ - - */ -class VariableCertaintyNullCoalesceRule implements \PHPStan\Rules\Rule -{ - - public function getNodeType(): string - { - return Node\Expr::class; - } - - public function processNode(Node $node, Scope $scope): array - { - if ($node instanceof Node\Expr\AssignOp\Coalesce) { - $var = $node->var; - $description = '??='; - } elseif ($node instanceof Node\Expr\BinaryOp\Coalesce) { - $var = $node->left; - $description = '??'; - } else { - return []; - } - - $isSubNode = false; - while ( - $var instanceof Node\Expr\ArrayDimFetch - || $var instanceof Node\Expr\PropertyFetch - || ( - $var instanceof Node\Expr\StaticPropertyFetch - && $var->class instanceof Node\Expr - ) - ) { - if ($var instanceof Node\Expr\StaticPropertyFetch) { - $var = $var->class; - } else { - $var = $var->var; - } - $isSubNode = true; - } - - if (!$var instanceof Node\Expr\Variable || !is_string($var->name)) { - return []; - } - - $certainty = $scope->hasVariableType($var->name); - if ($certainty->no()) { - return [RuleErrorBuilder::message(sprintf( - 'Variable $%s on left side of %s is never defined.', - $var->name, - $description - ))->build()]; - } elseif ($certainty->yes() && !$isSubNode) { - $variableType = $scope->getVariableType($var->name); - if ($variableType->isSuperTypeOf(new NullType())->no()) { - return [RuleErrorBuilder::message(sprintf( - 'Variable $%s on left side of %s always exists and is not nullable.', - $var->name, - $description - ))->build()]; - } elseif ((new NullType())->isSuperTypeOf($variableType)->yes()) { - return [RuleErrorBuilder::message(sprintf( - 'Variable $%s on left side of %s is always null.', - $var->name, - $description - ))->build()]; - } - } - - return []; - } - -} diff --git a/src/Rules/Variables/VariableCloningRule.php b/src/Rules/Variables/VariableCloningRule.php index 5caa3587f4..81bfbcb253 100644 --- a/src/Rules/Variables/VariableCloningRule.php +++ b/src/Rules/Variables/VariableCloningRule.php @@ -6,23 +6,23 @@ use PhpParser\Node\Expr\Clone_; use PhpParser\Node\Expr\Variable; use PHPStan\Analyser\Scope; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function is_string; +use function sprintf; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\Clone_> + * @implements Rule */ -class VariableCloningRule implements \PHPStan\Rules\Rule +class VariableCloningRule implements Rule { - private \PHPStan\Rules\RuleLevelHelper $ruleLevelHelper; - - public function __construct(RuleLevelHelper $ruleLevelHelper) + public function __construct(private RuleLevelHelper $ruleLevelHelper) { - $this->ruleLevelHelper = $ruleLevelHelper; } public function getNodeType(): string @@ -36,9 +36,7 @@ public function processNode(Node $node, Scope $scope): array $scope, $node->expr, 'Cloning object of an unknown class %s.', - static function (Type $type): bool { - return $type->isCloneable()->yes(); - } + static fn (Type $type): bool => $type->isCloneable()->yes(), ); $type = $typeResult->getType(); if ($type instanceof ErrorType) { @@ -53,7 +51,7 @@ static function (Type $type): bool { RuleErrorBuilder::message(sprintf( 'Cannot clone non-object variable $%s of type %s.', $node->expr->name, - $type->describe(VerbosityLevel::typeOnly()) + $type->describe(VerbosityLevel::typeOnly()), ))->build(), ]; } @@ -61,7 +59,7 @@ static function (Type $type): bool { return [ RuleErrorBuilder::message(sprintf( 'Cannot clone %s.', - $type->describe(VerbosityLevel::typeOnly()) + $type->describe(VerbosityLevel::typeOnly()), ))->build(), ]; } diff --git a/src/Rules/Whitespace/FileWhitespaceRule.php b/src/Rules/Whitespace/FileWhitespaceRule.php index 8829390f20..9a48d9293a 100644 --- a/src/Rules/Whitespace/FileWhitespaceRule.php +++ b/src/Rules/Whitespace/FileWhitespaceRule.php @@ -5,10 +5,12 @@ use Nette\Utils\Strings; use PhpParser\Node; use PhpParser\NodeTraverser; +use PhpParser\NodeVisitorAbstract; use PHPStan\Analyser\Scope; use PHPStan\Node\FileNode; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use function count; /** * @implements Rule @@ -35,13 +37,12 @@ public function processNode(Node $node, Scope $scope): array } $nodeTraverser = new NodeTraverser(); - $visitor = new class () extends \PhpParser\NodeVisitorAbstract { + $visitor = new class () extends NodeVisitorAbstract { - /** @var \PhpParser\Node[] */ - private $lastNodes = []; + /** @var Node[] */ + private array $lastNodes = []; /** - * @param Node $node * @return int|Node|null */ public function enterNode(Node $node) diff --git a/src/ShouldNotHappenException.php b/src/ShouldNotHappenException.php index e6317a757d..4f597f4b32 100644 --- a/src/ShouldNotHappenException.php +++ b/src/ShouldNotHappenException.php @@ -2,7 +2,9 @@ namespace PHPStan; -final class ShouldNotHappenException extends \Exception +use Exception; + +final class ShouldNotHappenException extends Exception { /** @api */ diff --git a/src/Testing/ErrorFormatterTestCase.php b/src/Testing/ErrorFormatterTestCase.php index 24f38c5848..40699ee725 100644 --- a/src/Testing/ErrorFormatterTestCase.php +++ b/src/Testing/ErrorFormatterTestCase.php @@ -8,10 +8,19 @@ use PHPStan\Command\Output; use PHPStan\Command\Symfony\SymfonyOutput; use PHPStan\Command\Symfony\SymfonyStyle; +use PHPStan\ShouldNotHappenException; use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Output\StreamOutput; - -abstract class ErrorFormatterTestCase extends \PHPStan\Testing\TestCase +use function array_map; +use function array_slice; +use function explode; +use function fopen; +use function implode; +use function rewind; +use function rtrim; +use function stream_get_contents; + +abstract class ErrorFormatterTestCase extends PHPStanTestCase { protected const DIRECTORY_PATH = '/data/folder/with space/and unicode 😃/project'; @@ -22,15 +31,12 @@ abstract class ErrorFormatterTestCase extends \PHPStan\Testing\TestCase private function getOutputStream(): StreamOutput { - if (PHP_VERSION_ID >= 80000 && DIRECTORY_SEPARATOR === '\\') { - $this->markTestSkipped('Skipped because of https://github.com/symfony/symfony/issues/37508'); - } if ($this->outputStream === null) { $resource = fopen('php://memory', 'w', false); if ($resource === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - $this->outputStream = new StreamOutput($resource); + $this->outputStream = new StreamOutput($resource, StreamOutput::VERBOSITY_NORMAL, false); } return $this->outputStream; @@ -52,7 +58,7 @@ protected function getOutputContent(): string $contents = stream_get_contents($this->getOutputStream()->getStream()); if ($contents === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $this->rtrimMultiline($contents); @@ -60,8 +66,8 @@ protected function getOutputContent(): string protected function getAnalysisResult(int $numFileErrors, int $numGenericErrors): AnalysisResult { - if ($numFileErrors > 4 || $numFileErrors < 0 || $numGenericErrors > 2 || $numGenericErrors < 0) { - throw new \PHPStan\ShouldNotHappenException(); + if ($numFileErrors > 5 || $numFileErrors < 0 || $numGenericErrors > 2 || $numGenericErrors < 0) { + throw new ShouldNotHappenException(); } $fileErrors = array_slice([ @@ -69,6 +75,7 @@ protected function getAnalysisResult(int $numFileErrors, int $numGenericErrors): new Error('Foo', self::DIRECTORY_PATH . '/foo.php', 1), new Error("Bar\nBar2", self::DIRECTORY_PATH . '/foo.php', 5), new Error("Bar\nBar2", self::DIRECTORY_PATH . '/folder with unicode 😃/file name with "spaces" and unicode 😃.php', 2), + new Error("Bar\nBar2", self::DIRECTORY_PATH . '/foo.php', null), ], 0, $numFileErrors); $genericErrors = array_slice([ @@ -83,15 +90,13 @@ protected function getAnalysisResult(int $numFileErrors, int $numGenericErrors): [], false, null, - true + true, ); } private function rtrimMultiline(string $output): string { - $result = array_map(static function (string $line): string { - return rtrim($line, " \r\n"); - }, explode("\n", $output)); + $result = array_map(static fn (string $line): string => rtrim($line, " \r\n"), explode("\n", $output)); return implode("\n", $result); } diff --git a/src/Testing/LevelsTestCase.php b/src/Testing/LevelsTestCase.php index 1a164ab44a..ca901a0dce 100644 --- a/src/Testing/LevelsTestCase.php +++ b/src/Testing/LevelsTestCase.php @@ -2,11 +2,28 @@ namespace PHPStan\Testing; +use Nette\Utils\Json; +use Nette\Utils\JsonException; use PHPStan\File\FileHelper; use PHPStan\File\FileWriter; +use PHPStan\ShouldNotHappenException; +use PHPUnit\Framework\AssertionFailedError; +use PHPUnit\Framework\TestCase; +use function array_merge; +use function count; +use function escapeshellarg; +use function escapeshellcmd; +use function exec; +use function implode; +use function method_exists; +use function range; +use function sprintf; +use function unlink; +use const DIRECTORY_SEPARATOR; +use const PHP_BINARY; /** @api */ -abstract class LevelsTestCase extends \PHPUnit\Framework\TestCase +abstract class LevelsTestCase extends TestCase { /** @@ -32,10 +49,9 @@ protected function shouldAutoloadAnalysedFile(): bool /** * @dataProvider dataTopics - * @param string $topic */ public function testLevels( - string $topic + string $topic, ): void { $file = sprintf('%s' . DIRECTORY_SEPARATOR . '%s.php', $this->getDataPath(), $topic); @@ -47,20 +63,20 @@ public function testLevels( $exceptions = []; - foreach (range(0, 8) as $level) { + foreach (range(0, 9) as $level) { unset($outputLines); exec(sprintf('%s %s clear-result-cache %s 2>&1', escapeshellarg(PHP_BINARY), $command, $configPath !== null ? '--configuration ' . escapeshellarg($configPath) : ''), $clearResultCacheOutputLines, $clearResultCacheExitCode); if ($clearResultCacheExitCode !== 0) { - throw new \PHPStan\ShouldNotHappenException('Could not clear result cache: ' . implode("\n", $clearResultCacheOutputLines)); + throw new ShouldNotHappenException('Could not clear result cache: ' . implode("\n", $clearResultCacheOutputLines)); } 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); $output = implode("\n", $outputLines); try { - $actualJson = \Nette\Utils\Json::decode($output, \Nette\Utils\Json::FORCE_ARRAY); - } catch (\Nette\Utils\JsonException $e) { - throw new \Nette\Utils\JsonException(sprintf('Cannot decode: %s', $output)); + $actualJson = Json::decode($output, Json::FORCE_ARRAY); + } catch (JsonException) { + throw new JsonException(sprintf('Cannot decode: %s', $output)); } if (count($actualJson['files']) > 0) { $normalizedFilePath = $fileHelper->normalizePath($file); @@ -141,30 +157,28 @@ public function getAdditionalAnalysedFiles(): array } /** - * @param string $expectedJsonFile * @param string[] $expectedMessages - * @return \PHPUnit\Framework\AssertionFailedError|null */ - private function compareFiles(string $expectedJsonFile, array $expectedMessages): ?\PHPUnit\Framework\AssertionFailedError + private function compareFiles(string $expectedJsonFile, array $expectedMessages): ?AssertionFailedError { if (count($expectedMessages) === 0) { try { self::assertFileDoesNotExist($expectedJsonFile); return null; - } catch (\PHPUnit\Framework\AssertionFailedError $e) { + } catch (AssertionFailedError $e) { unlink($expectedJsonFile); return $e; } } - $actualOutput = \Nette\Utils\Json::encode($expectedMessages, \Nette\Utils\Json::PRETTY); + $actualOutput = Json::encode($expectedMessages, Json::PRETTY); try { $this->assertJsonStringEqualsJsonFile( $expectedJsonFile, - $actualOutput + $actualOutput, ); - } catch (\PHPUnit\Framework\AssertionFailedError $e) { + } catch (AssertionFailedError $e) { FileWriter::write($expectedJsonFile, $actualOutput); return $e; } diff --git a/src/Testing/PHPStanTestCase.php b/src/Testing/PHPStanTestCase.php new file mode 100644 index 0000000000..3eb74e888c --- /dev/null +++ b/src/Testing/PHPStanTestCase.php @@ -0,0 +1,246 @@ + */ + private static array $containers = []; + + /** @api */ + public static function getContainer(): Container + { + $additionalConfigFiles = static::getAdditionalConfigFiles(); + $additionalConfigFiles[] = __DIR__ . '/TestCase.neon'; + if (self::$useStaticReflectionProvider) { + $additionalConfigFiles[] = __DIR__ . '/TestCase-staticReflection.neon'; + } + $cacheKey = sha1(implode("\n", $additionalConfigFiles)); + + 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)); + } + + $rootDir = __DIR__ . '/../..'; + $fileHelper = new FileHelper($rootDir); + $rootDir = $fileHelper->normalizePath($rootDir, '/'); + $containerFactory = new ContainerFactory($rootDir); + $container = $containerFactory->create($tmpDir, array_merge([ + $containerFactory->getConfigDirectory() . '/config.level8.neon', + ], $additionalConfigFiles), []); + self::$containers[$cacheKey] = $container; + + foreach ($container->getParameter('bootstrapFiles') as $bootstrapFile) { + (static function (string $file) use ($container): void { + require_once $file; + })($bootstrapFile); + } + + if (PHP_VERSION_ID >= 80000) { + require_once __DIR__ . '/../../stubs/runtime/Enum/UnitEnum.php'; + require_once __DIR__ . '/../../stubs/runtime/Enum/BackedEnum.php'; + require_once __DIR__ . '/../../stubs/runtime/Enum/ReflectionEnum.php'; + require_once __DIR__ . '/../../stubs/runtime/Enum/ReflectionEnumUnitCase.php'; + require_once __DIR__ . '/../../stubs/runtime/Enum/ReflectionEnumBackedCase.php'; + } + } + + return self::$containers[$cacheKey]; + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return []; + } + + public function getParser(): Parser + { + /** @var Parser $parser */ + $parser = self::getContainer()->getService('defaultAnalysisParser'); + return $parser; + } + + /** + * @api + * @deprecated Use createReflectionProvider() instead + */ + public function createBroker(): Broker + { + return self::getContainer()->getByType(Broker::class); + } + + /** @api */ + public function createReflectionProvider(): ReflectionProvider + { + return self::getContainer()->getByType(ReflectionProvider::class); + } + + public static function getReflector(): Reflector + { + return self::getContainer()->getService('betterReflectionReflector'); + } + + /** + * @deprecated Use getReflector() instead. + * @return array{ClassReflector, FunctionReflector, ConstantReflector} + */ + public static function getReflectors(): array + { + return [ + self::getContainer()->getService('betterReflectionClassReflector'), + self::getContainer()->getService('betterReflectionFunctionReflector'), + self::getContainer()->getService('betterReflectionConstantReflector'), + ]; + } + + public function getClassReflectionExtensionRegistryProvider(): ClassReflectionExtensionRegistryProvider + { + return self::getContainer()->getByType(ClassReflectionExtensionRegistryProvider::class); + } + + public function createScopeFactory(ReflectionProvider $reflectionProvider, TypeSpecifier $typeSpecifier): ScopeFactory + { + $container = self::getContainer(); + + return new DirectScopeFactory( + MutatingScope::class, + $reflectionProvider, + $container->getByType(DynamicReturnTypeExtensionRegistryProvider::class), + $container->getByType(OperatorTypeSpecifyingExtensionRegistryProvider::class), + new Standard(), + $typeSpecifier, + new PropertyReflectionFinder(), + $this->getParser(), + self::getContainer()->getByType(NodeScopeResolver::class), + $this->shouldTreatPhpDocTypesAsCertain(), + $container, + $container->getByType(PhpVersion::class), + $container->getParameter('featureToggles')['explicitMixedInUnknownGenericNew'], + ); + } + + /** + * @param array $globalTypeAliases + */ + public function createTypeAliasResolver(array $globalTypeAliases, ReflectionProvider $reflectionProvider): TypeAliasResolver + { + $container = self::getContainer(); + + return new UsefulTypeAliasResolver( + $globalTypeAliases, + $container->getByType(TypeStringResolver::class), + $container->getByType(TypeNodeResolver::class), + $reflectionProvider, + ); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return true; + } + + public function getFileHelper(): FileHelper + { + return self::getContainer()->getByType(FileHelper::class); + } + + /** + * Provides a DIRECTORY_SEPARATOR agnostic assertion helper, to compare file paths. + * + */ + protected function assertSamePaths(string $expected, string $actual, string $message = ''): void + { + $expected = $this->getFileHelper()->normalizePath($expected); + $actual = $this->getFileHelper()->normalizePath($actual); + + $this->assertSame($expected, $actual, $message); + } + + /** + * @param Error[]|string[] $errors + */ + protected function assertNoErrors(array $errors): void + { + try { + $this->assertCount(0, $errors); + } catch (ExpectationFailedException $e) { + $messages = []; + foreach ($errors as $error) { + if ($error instanceof Error) { + $messages[] = sprintf("- %s\n in %s on line %d\n", rtrim($error->getMessage(), '.'), $error->getFile(), $error->getLine()); + } else { + $messages[] = $error; + } + } + + $this->fail($e->getMessage() . "\n\nEmitted errors:\n" . implode("\n", $messages)); + } + } + + protected function skipIfNotOnWindows(): void + { + if (DIRECTORY_SEPARATOR === '\\') { + return; + } + + self::markTestSkipped(); + } + + protected function skipIfNotOnUnix(): void + { + if (DIRECTORY_SEPARATOR === '/') { + return; + } + + self::markTestSkipped(); + } + +} diff --git a/src/Testing/RuleTestCase.php b/src/Testing/RuleTestCase.php index 01f657065a..9751ad1fb1 100644 --- a/src/Testing/RuleTestCase.php +++ b/src/Testing/RuleTestCase.php @@ -7,45 +7,37 @@ use PHPStan\Analyser\FileAnalyser; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\TypeSpecifier; -use PHPStan\Broker\AnonymousClassNameHelper; -use PHPStan\Cache\Cache; use PHPStan\Dependency\DependencyResolver; -use PHPStan\Dependency\ExportedNodeResolver; use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; use PHPStan\File\FileHelper; -use PHPStan\File\SimpleRelativePathHelper; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocInheritanceResolver; -use PHPStan\PhpDoc\PhpDocNodeResolver; -use PHPStan\PhpDoc\PhpDocStringResolver; -use PHPStan\Reflection\ReflectionProvider\DirectReflectionProviderProvider; +use PHPStan\PhpDoc\StubPhpDocProvider; use PHPStan\Rules\Registry; use PHPStan\Rules\Rule; use PHPStan\Type\FileTypeMapper; +use function array_map; +use function count; +use function implode; +use function sprintf; /** * @api - * @template TRule of \PHPStan\Rules\Rule + * @template TRule of Rule */ -abstract class RuleTestCase extends \PHPStan\Testing\TestCase +abstract class RuleTestCase extends PHPStanTestCase { - private ?\PHPStan\Analyser\Analyser $analyser = null; + private ?Analyser $analyser = null; /** - * @return \PHPStan\Rules\Rule * @phpstan-return TRule */ abstract protected function getRule(): Rule; protected function getTypeSpecifier(): TypeSpecifier { - return $this->createTypeSpecifier( - new \PhpParser\PrettyPrinter\Standard(), - $this->createReflectionProvider(), - $this->getMethodTypeSpecifyingExtensions(), - $this->getStaticMethodTypeSpecifyingExtensions() - ); + return self::getContainer()->getService('typeSpecifier'); } private function getAnalyser(): Analyser @@ -55,78 +47,47 @@ private function getAnalyser(): Analyser $this->getRule(), ]); - $broker = $this->createBroker(); - $printer = new \PhpParser\PrettyPrinter\Standard(); - $typeSpecifier = $this->createTypeSpecifier( - $printer, - $broker, - $this->getMethodTypeSpecifyingExtensions(), - $this->getStaticMethodTypeSpecifyingExtensions() - ); - $currentWorkingDirectory = $this->getCurrentWorkingDirectory(); - $fileHelper = new FileHelper($currentWorkingDirectory); - $currentWorkingDirectory = $fileHelper->normalizePath($currentWorkingDirectory, '/'); - $fileHelper = new FileHelper($currentWorkingDirectory); - $relativePathHelper = new SimpleRelativePathHelper($currentWorkingDirectory); - $anonymousClassNameHelper = new AnonymousClassNameHelper($fileHelper, $relativePathHelper); - $fileTypeMapper = new FileTypeMapper(new DirectReflectionProviderProvider($broker), $this->getParser(), self::getContainer()->getByType(PhpDocStringResolver::class), self::getContainer()->getByType(PhpDocNodeResolver::class), $this->createMock(Cache::class), $anonymousClassNameHelper); - $phpDocInheritanceResolver = new PhpDocInheritanceResolver($fileTypeMapper); + $reflectionProvider = $this->createReflectionProvider(); + $typeSpecifier = $this->getTypeSpecifier(); $nodeScopeResolver = new NodeScopeResolver( - $broker, - self::getReflectors()[0], + $reflectionProvider, + self::getReflector(), $this->getClassReflectionExtensionRegistryProvider(), $this->getParser(), - $fileTypeMapper, + self::getContainer()->getByType(FileTypeMapper::class), + self::getContainer()->getByType(StubPhpDocProvider::class), self::getContainer()->getByType(PhpVersion::class), - $phpDocInheritanceResolver, - $fileHelper, + self::getContainer()->getByType(PhpDocInheritanceResolver::class), + self::getContainer()->getByType(FileHelper::class), $typeSpecifier, self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class), $this->shouldPolluteScopeWithLoopInitialAssignments(), - $this->shouldPolluteCatchScopeWithTryAssignments(), $this->shouldPolluteScopeWithAlwaysIterableForeach(), [], [], true, - true ); $fileAnalyser = new FileAnalyser( - $this->createScopeFactory($broker, $typeSpecifier), + $this->createScopeFactory($reflectionProvider, $typeSpecifier), $nodeScopeResolver, $this->getParser(), - new DependencyResolver($fileHelper, $broker, new ExportedNodeResolver($fileTypeMapper, $printer)), - true + self::getContainer()->getByType(DependencyResolver::class), + true, ); $this->analyser = new Analyser( $fileAnalyser, $registry, $nodeScopeResolver, - 50 + 50, ); } return $this->analyser; } - /** - * @return \PHPStan\Type\MethodTypeSpecifyingExtension[] - */ - protected function getMethodTypeSpecifyingExtensions(): array - { - return []; - } - - /** - * @return \PHPStan\Type\StaticMethodTypeSpecifyingExtension[] - */ - protected function getStaticMethodTypeSpecifyingExtensions(): array - { - return []; - } - /** * @param string[] $files - * @param mixed[] $expectedErrors + * @param list $expectedErrors */ public function analyse(array $files, array $expectedErrors): void { @@ -135,7 +96,7 @@ public function analyse(array $files, array $expectedErrors): void $files, null, null, - true + true, ); if (count($analyserResult->getInternalErrors()) > 0) { $this->fail(implode("\n", $analyserResult->getInternalErrors())); @@ -152,16 +113,8 @@ public function analyse(array $files, array $expectedErrors): void }; $expectedErrors = array_map( - static function (array $error) use ($strictlyTypedSprintf): string { - if (!isset($error[0])) { - throw new \InvalidArgumentException('Missing expected error message.'); - } - if (!isset($error[1])) { - throw new \InvalidArgumentException('Missing expected file line.'); - } - return $strictlyTypedSprintf($error[1], $error[0], $error[2] ?? null); - }, - $expectedErrors + static fn (array $error): string => $strictlyTypedSprintf($error[1], $error[0], $error[2] ?? null), + $expectedErrors, ); $actualErrors = array_map( @@ -172,7 +125,7 @@ static function (Error $error) use ($strictlyTypedSprintf): string { } return $strictlyTypedSprintf($line, $error->getMessage(), $error->getTip()); }, - $actualErrors + $actualErrors, ); $this->assertSame(implode("\n", $expectedErrors) . "\n", implode("\n", $actualErrors) . "\n"); @@ -183,11 +136,6 @@ protected function shouldPolluteScopeWithLoopInitialAssignments(): bool return false; } - protected function shouldPolluteCatchScopeWithTryAssignments(): bool - { - return false; - } - protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool { return true; diff --git a/src/Testing/TestCase-staticReflection.neon b/src/Testing/TestCase-staticReflection.neon index b79fe6b608..6da322a517 100644 --- a/src/Testing/TestCase-staticReflection.neon +++ b/src/Testing/TestCase-staticReflection.neon @@ -12,40 +12,13 @@ services: options: usedAttributes: [comments, startLine, endLine, startTokenPos, endTokenPos] - testCaseBetterReflectionProvider: - class: PHPStan\Reflection\BetterReflection\BetterReflectionProvider - arguments: - classReflector: @testCaseClassReflector - functionReflector: @testCaseFunctionReflector - constantReflector: @testCaseConstantReflector - autowired: false - - testCaseClassReflector: - class: PHPStan\Reflection\BetterReflection\Reflector\MemoizingClassReflector - arguments: - sourceLocator: @testCaseSourceLocator - autowired: false - - testCaseFunctionReflector: - class: PHPStan\Reflection\BetterReflection\Reflector\MemoizingFunctionReflector - arguments: - classReflector: @testCaseClassReflector - sourceLocator: @testCaseSourceLocator - autowired: false - - testCaseConstantReflector: - class: PHPStan\Reflection\BetterReflection\Reflector\MemoizingConstantReflector - arguments: - classReflector: @testCaseClassReflector - sourceLocator: @testCaseSourceLocator - autowired: false - - testCaseSourceLocator: + betterReflectionSourceLocator: class: PHPStan\BetterReflection\SourceLocator\Type\SourceLocator factory: @PHPStan\Testing\TestCaseSourceLocatorFactory::create() autowired: false reflectionProvider: - factory: @testCaseBetterReflectionProvider + factory: @betterReflectionProvider + arguments!: [] autowired: - PHPStan\Reflection\ReflectionProvider diff --git a/src/Testing/TestCase.neon b/src/Testing/TestCase.neon new file mode 100644 index 0000000000..de27420e8b --- /dev/null +++ b/src/Testing/TestCase.neon @@ -0,0 +1,8 @@ +parameters: + inferPrivatePropertyTypeFromConstructor: true +services: + cacheStorage: + class: PHPStan\Cache\MemoryCacheStorage + arguments!: [] + currentPhpVersionSimpleParser!: + factory: @currentPhpVersionRichParser diff --git a/src/Testing/TestCase.php b/src/Testing/TestCase.php deleted file mode 100644 index ec3ccb90e1..0000000000 --- a/src/Testing/TestCase.php +++ /dev/null @@ -1,703 +0,0 @@ - */ - private static array $containers = []; - - private ?DirectClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider = null; - - /** @var array{ClassReflector, FunctionReflector, ConstantReflector}|null */ - private static $reflectors; - - /** @var PhpStormStubsSourceStubber|null */ - private static $phpStormStubsSourceStubber; - - /** @api */ - public static function getContainer(): Container - { - $additionalConfigFiles = static::getAdditionalConfigFiles(); - $cacheKey = sha1(implode("\n", $additionalConfigFiles)); - - 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)); - } - - if (self::$useStaticReflectionProvider) { - $additionalConfigFiles[] = __DIR__ . '/TestCase-staticReflection.neon'; - } - - $rootDir = __DIR__ . '/../..'; - $containerFactory = new ContainerFactory($rootDir); - $container = $containerFactory->create($tmpDir, array_merge([ - $containerFactory->getConfigDirectory() . '/config.level8.neon', - ], $additionalConfigFiles), []); - self::$containers[$cacheKey] = $container; - - foreach ($container->getParameter('bootstrapFiles') as $bootstrapFile) { - (static function (string $file) use ($container): void { - require_once $file; - })($bootstrapFile); - } - } - - return self::$containers[$cacheKey]; - } - - /** - * @return string[] - */ - public static function getAdditionalConfigFiles(): array - { - return []; - } - - public function getParser(): \PHPStan\Parser\Parser - { - /** @var \PHPStan\Parser\Parser $parser */ - $parser = self::getContainer()->getByType(CachedParser::class); - return $parser; - } - - /** - * @api - * @param \PHPStan\Type\DynamicMethodReturnTypeExtension[] $dynamicMethodReturnTypeExtensions - * @param \PHPStan\Type\DynamicStaticMethodReturnTypeExtension[] $dynamicStaticMethodReturnTypeExtensions - * @return \PHPStan\Broker\Broker - */ - public function createBroker( - array $dynamicMethodReturnTypeExtensions = [], - array $dynamicStaticMethodReturnTypeExtensions = [] - ): Broker - { - $dynamicReturnTypeExtensionRegistryProvider = new DirectDynamicReturnTypeExtensionRegistryProvider( - array_merge(self::getContainer()->getServicesByTag(BrokerFactory::DYNAMIC_METHOD_RETURN_TYPE_EXTENSION_TAG), $dynamicMethodReturnTypeExtensions, $this->getDynamicMethodReturnTypeExtensions()), - array_merge(self::getContainer()->getServicesByTag(BrokerFactory::DYNAMIC_STATIC_METHOD_RETURN_TYPE_EXTENSION_TAG), $dynamicStaticMethodReturnTypeExtensions, $this->getDynamicStaticMethodReturnTypeExtensions()), - array_merge(self::getContainer()->getServicesByTag(BrokerFactory::DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG), $this->getDynamicFunctionReturnTypeExtensions()) - ); - $operatorTypeSpecifyingExtensionRegistryProvider = new DirectOperatorTypeSpecifyingExtensionRegistryProvider( - $this->getOperatorTypeSpecifyingExtensions() - ); - $reflectionProvider = $this->createReflectionProvider(); - $broker = new Broker( - $reflectionProvider, - $dynamicReturnTypeExtensionRegistryProvider, - $operatorTypeSpecifyingExtensionRegistryProvider, - self::getContainer()->getParameter('universalObjectCratesClasses') - ); - $dynamicReturnTypeExtensionRegistryProvider->setBroker($broker); - $dynamicReturnTypeExtensionRegistryProvider->setReflectionProvider($reflectionProvider); - $operatorTypeSpecifyingExtensionRegistryProvider->setBroker($broker); - $this->getClassReflectionExtensionRegistryProvider()->setBroker($broker); - - return $broker; - } - - /** @api */ - public function createReflectionProvider(): ReflectionProvider - { - $setterReflectionProviderProvider = new ReflectionProvider\SetterReflectionProviderProvider(); - $staticReflectionProvider = $this->createStaticReflectionProvider($setterReflectionProviderProvider); - $reflectionProvider = $this->createReflectionProviderByParameters( - $this->createRuntimeReflectionProvider($setterReflectionProviderProvider), - $staticReflectionProvider, - self::$useStaticReflectionProvider - ); - $setterReflectionProviderProvider->setReflectionProvider($reflectionProvider); - - return $reflectionProvider; - } - - private function createReflectionProviderByParameters( - ReflectionProvider $runtimeReflectionProvider, - ReflectionProvider $staticReflectionProvider, - bool $disableRuntimeReflectionProvider - ): ReflectionProvider - { - $reflectionProviderFactory = new ReflectionProviderFactory( - $runtimeReflectionProvider, - $staticReflectionProvider, - $disableRuntimeReflectionProvider - ); - - return $reflectionProviderFactory->create(); - } - - private static function getPhpStormStubsSourceStubber(): PhpStormStubsSourceStubber - { - if (self::$phpStormStubsSourceStubber === null) { - self::$phpStormStubsSourceStubber = self::getContainer()->getByType(PhpStormStubsSourceStubber::class); - } - - return self::$phpStormStubsSourceStubber; - } - - private function createRuntimeReflectionProvider( - ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider - ): ReflectionProvider - { - $functionCallStatementFinder = new FunctionCallStatementFinder(); - $parser = $this->getParser(); - $cache = new Cache(new MemoryCacheStorage()); - $phpDocStringResolver = self::getContainer()->getByType(PhpDocStringResolver::class); - $phpDocNodeResolver = self::getContainer()->getByType(PhpDocNodeResolver::class); - $currentWorkingDirectory = $this->getCurrentWorkingDirectory(); - $fileHelper = new FileHelper($currentWorkingDirectory); - $anonymousClassNameHelper = new AnonymousClassNameHelper(new FileHelper($currentWorkingDirectory), new SimpleRelativePathHelper($fileHelper->normalizePath($currentWorkingDirectory, '/'))); - $fileTypeMapper = new FileTypeMapper($reflectionProviderProvider, $parser, $phpDocStringResolver, $phpDocNodeResolver, $cache, $anonymousClassNameHelper); - $classReflectionExtensionRegistryProvider = $this->getClassReflectionExtensionRegistryProvider(); - $functionReflectionFactory = $this->getFunctionReflectionFactory( - $functionCallStatementFinder, - $cache - ); - $reflectionProvider = new ClassBlacklistReflectionProvider( - new RuntimeReflectionProvider( - $reflectionProviderProvider, - $classReflectionExtensionRegistryProvider, - $functionReflectionFactory, - $fileTypeMapper, - self::getContainer()->getByType(PhpVersion::class), - self::getContainer()->getByType(NativeFunctionReflectionProvider::class), - self::getContainer()->getByType(StubPhpDocProvider::class), - self::getContainer()->getByType(PhpStormStubsSourceStubber::class) - ), - self::getPhpStormStubsSourceStubber(), - [ - '#^PhpParser\\\\#', - '#^PHPStan\\\\#', - '#^Hoa\\\\#', - ], - null - ); - $this->setUpReflectionProvider( - $reflectionProviderProvider, - $classReflectionExtensionRegistryProvider, - $functionCallStatementFinder, - $parser, - $cache, - $fileTypeMapper - ); - - return $reflectionProvider; - } - - private function setUpReflectionProvider( - ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, - DirectClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider, - FunctionCallStatementFinder $functionCallStatementFinder, - \PHPStan\Parser\Parser $parser, - Cache $cache, - FileTypeMapper $fileTypeMapper - ): void - { - $methodReflectionFactory = new class($parser, $functionCallStatementFinder, $cache, $reflectionProviderProvider) implements PhpMethodReflectionFactory { - - private \PHPStan\Parser\Parser $parser; - - private \PHPStan\Parser\FunctionCallStatementFinder $functionCallStatementFinder; - - private \PHPStan\Cache\Cache $cache; - - private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider; - - public function __construct( - Parser $parser, - FunctionCallStatementFinder $functionCallStatementFinder, - Cache $cache, - ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider - ) - { - $this->parser = $parser; - $this->functionCallStatementFinder = $functionCallStatementFinder; - $this->cache = $cache; - $this->reflectionProviderProvider = $reflectionProviderProvider; - } - - /** - * @param ClassReflection $declaringClass - * @param ClassReflection|null $declaringTrait - * @param \PHPStan\Reflection\Php\BuiltinMethodReflection $reflection - * @param TemplateTypeMap $templateTypeMap - * @param Type[] $phpDocParameterTypes - * @param Type|null $phpDocReturnType - * @param Type|null $phpDocThrowType - * @param string|null $deprecatedDescription - * @param bool $isDeprecated - * @param bool $isInternal - * @param bool $isFinal - * @param string|null $stubPhpDocString - * @param bool|null $isPure - * @return PhpMethodReflection - */ - public function create( - ClassReflection $declaringClass, - ?ClassReflection $declaringTrait, - \PHPStan\Reflection\Php\BuiltinMethodReflection $reflection, - TemplateTypeMap $templateTypeMap, - array $phpDocParameterTypes, - ?Type $phpDocReturnType, - ?Type $phpDocThrowType, - ?string $deprecatedDescription, - bool $isDeprecated, - bool $isInternal, - bool $isFinal, - ?string $stubPhpDocString, - ?bool $isPure = null - ): PhpMethodReflection - { - return new PhpMethodReflection( - $declaringClass, - $declaringTrait, - $reflection, - $this->reflectionProviderProvider->getReflectionProvider(), - $this->parser, - $this->functionCallStatementFinder, - $this->cache, - $templateTypeMap, - $phpDocParameterTypes, - $phpDocReturnType, - $phpDocThrowType, - $deprecatedDescription, - $isDeprecated, - $isInternal, - $isFinal, - $stubPhpDocString, - $isPure - ); - } - - }; - $phpDocInheritanceResolver = new PhpDocInheritanceResolver($fileTypeMapper); - $annotationsMethodsClassReflectionExtension = new AnnotationsMethodsClassReflectionExtension(); - $annotationsPropertiesClassReflectionExtension = new AnnotationsPropertiesClassReflectionExtension(); - $signatureMapProvider = self::getContainer()->getByType(SignatureMapProvider::class); - $phpExtension = new PhpClassReflectionExtension(self::getContainer()->getByType(ScopeFactory::class), self::getContainer()->getByType(NodeScopeResolver::class), $methodReflectionFactory, $phpDocInheritanceResolver, $annotationsMethodsClassReflectionExtension, $annotationsPropertiesClassReflectionExtension, $signatureMapProvider, $parser, self::getContainer()->getByType(StubPhpDocProvider::class), $reflectionProviderProvider, $fileTypeMapper, true, []); - $classReflectionExtensionRegistryProvider->addPropertiesClassReflectionExtension($phpExtension); - $classReflectionExtensionRegistryProvider->addPropertiesClassReflectionExtension(new UniversalObjectCratesClassReflectionExtension([\stdClass::class])); - $classReflectionExtensionRegistryProvider->addPropertiesClassReflectionExtension(new MixinPropertiesClassReflectionExtension([])); - $classReflectionExtensionRegistryProvider->addPropertiesClassReflectionExtension(new SimpleXMLElementClassPropertyReflectionExtension()); - $classReflectionExtensionRegistryProvider->addPropertiesClassReflectionExtension($annotationsPropertiesClassReflectionExtension); - $classReflectionExtensionRegistryProvider->addMethodsClassReflectionExtension($phpExtension); - $classReflectionExtensionRegistryProvider->addMethodsClassReflectionExtension(new MixinMethodsClassReflectionExtension([])); - $classReflectionExtensionRegistryProvider->addMethodsClassReflectionExtension($annotationsMethodsClassReflectionExtension); - $classReflectionExtensionRegistryProvider->addMethodsClassReflectionExtension(new SoapClientMethodsClassReflectionExtension()); - } - - private function createStaticReflectionProvider( - ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider - ): ReflectionProvider - { - $parser = $this->getParser(); - $phpDocStringResolver = self::getContainer()->getByType(PhpDocStringResolver::class); - $phpDocNodeResolver = self::getContainer()->getByType(PhpDocNodeResolver::class); - $currentWorkingDirectory = $this->getCurrentWorkingDirectory(); - $cache = new Cache(new MemoryCacheStorage()); - $fileHelper = new FileHelper($currentWorkingDirectory); - $relativePathHelper = new SimpleRelativePathHelper($currentWorkingDirectory); - $anonymousClassNameHelper = new AnonymousClassNameHelper($fileHelper, new SimpleRelativePathHelper($fileHelper->normalizePath($currentWorkingDirectory, '/'))); - $fileTypeMapper = new FileTypeMapper($reflectionProviderProvider, $parser, $phpDocStringResolver, $phpDocNodeResolver, $cache, $anonymousClassNameHelper); - $functionCallStatementFinder = new FunctionCallStatementFinder(); - $functionReflectionFactory = $this->getFunctionReflectionFactory( - $functionCallStatementFinder, - $cache - ); - - [$classReflector, $functionReflector, $constantReflector] = self::getReflectors(); - - $classReflectionExtensionRegistryProvider = $this->getClassReflectionExtensionRegistryProvider(); - - $reflectionProvider = new BetterReflectionProvider( - $reflectionProviderProvider, - $classReflectionExtensionRegistryProvider, - $classReflector, - $fileTypeMapper, - self::getContainer()->getByType(PhpVersion::class), - self::getContainer()->getByType(NativeFunctionReflectionProvider::class), - self::getContainer()->getByType(StubPhpDocProvider::class), - $functionReflectionFactory, - $relativePathHelper, - $anonymousClassNameHelper, - self::getContainer()->getByType(Standard::class), - $fileHelper, - $functionReflector, - $constantReflector, - self::getPhpStormStubsSourceStubber() - ); - - $this->setUpReflectionProvider( - $reflectionProviderProvider, - $classReflectionExtensionRegistryProvider, - $functionCallStatementFinder, - $parser, - $cache, - $fileTypeMapper - ); - - return $reflectionProvider; - } - - /** - * @return array{ClassReflector, FunctionReflector, ConstantReflector} - */ - public static function getReflectors(): array - { - if (self::$reflectors !== null) { - return self::$reflectors; - } - - if (!class_exists(ClassLoader::class)) { - self::fail('Composer ClassLoader is unknown'); - } - - $classLoaderReflection = new \ReflectionClass(ClassLoader::class); - if ($classLoaderReflection->getFileName() === false) { - self::fail('Unknown ClassLoader filename'); - } - - $composerProjectPath = dirname($classLoaderReflection->getFileName(), 3); - if (!is_file($composerProjectPath . '/composer.json')) { - self::fail(sprintf('composer.json not found in directory %s', $composerProjectPath)); - } - - $composerJsonAndInstalledJsonSourceLocatorMaker = self::getContainer()->getByType(ComposerJsonAndInstalledJsonSourceLocatorMaker::class); - $composerSourceLocator = $composerJsonAndInstalledJsonSourceLocatorMaker->create($composerProjectPath); - if ($composerSourceLocator === null) { - self::fail('Could not create composer source locator'); - } - - // these need to be synced with TestCase-staticReflection.neon file and TestCaseSourceLocatorFactory - - $locators = [ - $composerSourceLocator, - ]; - - $phpParser = new PhpParserDecorator(self::getContainer()->getByType(CachedParser::class)); - - /** @var FunctionReflector $functionReflector */ - $functionReflector = null; - $astLocator = new Locator($phpParser, static function () use (&$functionReflector): FunctionReflector { - return $functionReflector; - }); - $astPhp8Locator = new Locator(self::getContainer()->getService('php8PhpParser'), static function () use (&$functionReflector): FunctionReflector { - return $functionReflector; - }); - $reflectionSourceStubber = new ReflectionSourceStubber(); - $locators[] = new PhpInternalSourceLocator($astPhp8Locator, self::getPhpStormStubsSourceStubber()); - $locators[] = new AutoloadSourceLocator(self::getContainer()->getByType(FileNodesFetcher::class)); - $locators[] = new PhpInternalSourceLocator($astLocator, $reflectionSourceStubber); - $locators[] = new EvaledCodeSourceLocator($astLocator, $reflectionSourceStubber); - $sourceLocator = new MemoizingSourceLocator(new AggregateSourceLocator($locators)); - - $classReflector = new MemoizingClassReflector($sourceLocator); - $functionReflector = new MemoizingFunctionReflector($sourceLocator, $classReflector); - $constantReflector = new MemoizingConstantReflector($sourceLocator, $classReflector); - - self::$reflectors = [$classReflector, $functionReflector, $constantReflector]; - - return self::$reflectors; - } - - private function getFunctionReflectionFactory( - FunctionCallStatementFinder $functionCallStatementFinder, - Cache $cache - ): FunctionReflectionFactory - { - return new class($this->getParser(), $functionCallStatementFinder, $cache) implements FunctionReflectionFactory { - - private \PHPStan\Parser\Parser $parser; - - private \PHPStan\Parser\FunctionCallStatementFinder $functionCallStatementFinder; - - private \PHPStan\Cache\Cache $cache; - - public function __construct( - Parser $parser, - FunctionCallStatementFinder $functionCallStatementFinder, - Cache $cache - ) - { - $this->parser = $parser; - $this->functionCallStatementFinder = $functionCallStatementFinder; - $this->cache = $cache; - } - - /** - * @param \ReflectionFunction $function - * @param TemplateTypeMap $templateTypeMap - * @param Type[] $phpDocParameterTypes - * @param Type|null $phpDocReturnType - * @param Type|null $phpDocThrowType - * @param string|null $deprecatedDescription - * @param bool $isDeprecated - * @param bool $isInternal - * @param bool $isFinal - * @param string|false $filename - * @param bool|null $isPure - * @return PhpFunctionReflection - */ - public function create( - \ReflectionFunction $function, - TemplateTypeMap $templateTypeMap, - array $phpDocParameterTypes, - ?Type $phpDocReturnType, - ?Type $phpDocThrowType, - ?string $deprecatedDescription, - bool $isDeprecated, - bool $isInternal, - bool $isFinal, - $filename, - ?bool $isPure = null - ): PhpFunctionReflection - { - return new PhpFunctionReflection( - $function, - $this->parser, - $this->functionCallStatementFinder, - $this->cache, - $templateTypeMap, - $phpDocParameterTypes, - $phpDocReturnType, - $phpDocThrowType, - $deprecatedDescription, - $isDeprecated, - $isInternal, - $isFinal, - $filename, - $isPure - ); - } - - }; - } - - public function getClassReflectionExtensionRegistryProvider(): DirectClassReflectionExtensionRegistryProvider - { - if ($this->classReflectionExtensionRegistryProvider === null) { - $this->classReflectionExtensionRegistryProvider = new DirectClassReflectionExtensionRegistryProvider([], []); - } - - return $this->classReflectionExtensionRegistryProvider; - } - - public function createScopeFactory(Broker $broker, TypeSpecifier $typeSpecifier): ScopeFactory - { - $container = self::getContainer(); - - return new DirectScopeFactory( - MutatingScope::class, - $broker, - $broker->getDynamicReturnTypeExtensionRegistryProvider(), - $broker->getOperatorTypeSpecifyingExtensionRegistryProvider(), - new \PhpParser\PrettyPrinter\Standard(), - $typeSpecifier, - new PropertyReflectionFinder(), - $this->getParser(), - self::getContainer()->getByType(NodeScopeResolver::class), - $this->shouldTreatPhpDocTypesAsCertain(), - false, - $container - ); - } - - /** - * @param array $globalTypeAliases - */ - public function createTypeAliasResolver(array $globalTypeAliases, ReflectionProvider $reflectionProvider): TypeAliasResolver - { - $container = self::getContainer(); - - return new TypeAliasResolver( - $globalTypeAliases, - $container->getByType(TypeStringResolver::class), - $container->getByType(TypeNodeResolver::class), - $reflectionProvider - ); - } - - protected function shouldTreatPhpDocTypesAsCertain(): bool - { - return true; - } - - public function getCurrentWorkingDirectory(): string - { - return $this->getFileHelper()->normalizePath(__DIR__ . '/../..'); - } - - /** - * @return \PHPStan\Type\DynamicMethodReturnTypeExtension[] - */ - public function getDynamicMethodReturnTypeExtensions(): array - { - return []; - } - - /** - * @return \PHPStan\Type\DynamicStaticMethodReturnTypeExtension[] - */ - public function getDynamicStaticMethodReturnTypeExtensions(): array - { - return []; - } - - /** - * @return \PHPStan\Type\DynamicFunctionReturnTypeExtension[] - */ - public function getDynamicFunctionReturnTypeExtensions(): array - { - return []; - } - - /** - * @return \PHPStan\Type\OperatorTypeSpecifyingExtension[] - */ - public function getOperatorTypeSpecifyingExtensions(): array - { - return []; - } - - /** - * @param \PhpParser\PrettyPrinter\Standard $printer - * @param \PHPStan\Reflection\ReflectionProvider $reflectionProvider - * @param \PHPStan\Type\MethodTypeSpecifyingExtension[] $methodTypeSpecifyingExtensions - * @param \PHPStan\Type\StaticMethodTypeSpecifyingExtension[] $staticMethodTypeSpecifyingExtensions - * @return \PHPStan\Analyser\TypeSpecifier - */ - public function createTypeSpecifier( - Standard $printer, - ReflectionProvider $reflectionProvider, - array $methodTypeSpecifyingExtensions = [], - array $staticMethodTypeSpecifyingExtensions = [] - ): TypeSpecifier - { - return new TypeSpecifier( - $printer, - $reflectionProvider, - true, - self::getContainer()->getServicesByTag(TypeSpecifierFactory::FUNCTION_TYPE_SPECIFYING_EXTENSION_TAG), - array_merge($methodTypeSpecifyingExtensions, self::getContainer()->getServicesByTag(TypeSpecifierFactory::METHOD_TYPE_SPECIFYING_EXTENSION_TAG)), - array_merge($staticMethodTypeSpecifyingExtensions, self::getContainer()->getServicesByTag(TypeSpecifierFactory::STATIC_METHOD_TYPE_SPECIFYING_EXTENSION_TAG)) - ); - } - - public function getFileHelper(): FileHelper - { - return self::getContainer()->getByType(FileHelper::class); - } - - /** - * Provides a DIRECTORY_SEPARATOR agnostic assertion helper, to compare file paths. - * - * @param string $expected - * @param string $actual - * @param string $message - */ - protected function assertSamePaths(string $expected, string $actual, string $message = ''): void - { - $expected = $this->getFileHelper()->normalizePath($expected); - $actual = $this->getFileHelper()->normalizePath($actual); - - $this->assertSame($expected, $actual, $message); - } - - protected function skipIfNotOnWindows(): void - { - if (DIRECTORY_SEPARATOR === '\\') { - return; - } - - self::markTestSkipped(); - } - - protected function skipIfNotOnUnix(): void - { - if (DIRECTORY_SEPARATOR === '/') { - return; - } - - self::markTestSkipped(); - } - -} diff --git a/src/Testing/TestCaseSourceLocatorFactory.php b/src/Testing/TestCaseSourceLocatorFactory.php index 99a6dc38d3..16b94cae23 100644 --- a/src/Testing/TestCaseSourceLocatorFactory.php +++ b/src/Testing/TestCaseSourceLocatorFactory.php @@ -3,7 +3,7 @@ namespace PHPStan\Testing; use Composer\Autoload\ClassLoader; -use PHPStan\BetterReflection\Reflector\FunctionReflector; +use PhpParser\Parser; use PHPStan\BetterReflection\SourceLocator\Ast\Locator; use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; use PHPStan\BetterReflection\SourceLocator\SourceStubber\ReflectionSourceStubber; @@ -12,73 +12,51 @@ use PHPStan\BetterReflection\SourceLocator\Type\MemoizingSourceLocator; use PHPStan\BetterReflection\SourceLocator\Type\PhpInternalSourceLocator; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; -use PHPStan\DependencyInjection\Container; use PHPStan\Reflection\BetterReflection\SourceLocator\AutoloadSourceLocator; use PHPStan\Reflection\BetterReflection\SourceLocator\ComposerJsonAndInstalledJsonSourceLocatorMaker; use PHPStan\Reflection\BetterReflection\SourceLocator\PhpVersionBlacklistSourceLocator; +use ReflectionClass; +use function dirname; +use function is_file; class TestCaseSourceLocatorFactory { - private Container $container; - - private ComposerJsonAndInstalledJsonSourceLocatorMaker $composerJsonAndInstalledJsonSourceLocatorMaker; - - private AutoloadSourceLocator $autoloadSourceLocator; - - private \PhpParser\Parser $phpParser; - - private \PhpParser\Parser $php8Parser; - - private PhpStormStubsSourceStubber $phpstormStubsSourceStubber; - - private ReflectionSourceStubber $reflectionSourceStubber; - public function __construct( - Container $container, - ComposerJsonAndInstalledJsonSourceLocatorMaker $composerJsonAndInstalledJsonSourceLocatorMaker, - AutoloadSourceLocator $autoloadSourceLocator, - \PhpParser\Parser $phpParser, - \PhpParser\Parser $php8Parser, - PhpStormStubsSourceStubber $phpstormStubsSourceStubber, - ReflectionSourceStubber $reflectionSourceStubber + private ComposerJsonAndInstalledJsonSourceLocatorMaker $composerJsonAndInstalledJsonSourceLocatorMaker, + private AutoloadSourceLocator $autoloadSourceLocator, + private Parser $phpParser, + private Parser $php8Parser, + private PhpStormStubsSourceStubber $phpstormStubsSourceStubber, + private ReflectionSourceStubber $reflectionSourceStubber, ) { - $this->container = $container; - $this->composerJsonAndInstalledJsonSourceLocatorMaker = $composerJsonAndInstalledJsonSourceLocatorMaker; - $this->autoloadSourceLocator = $autoloadSourceLocator; - $this->phpParser = $phpParser; - $this->php8Parser = $php8Parser; - $this->phpstormStubsSourceStubber = $phpstormStubsSourceStubber; - $this->reflectionSourceStubber = $reflectionSourceStubber; } public function create(): SourceLocator { - $classLoaderReflection = new \ReflectionClass(ClassLoader::class); - if ($classLoaderReflection->getFileName() === false) { - throw new \PHPStan\ShouldNotHappenException('Unknown ClassLoader filename'); - } - - $composerProjectPath = dirname($classLoaderReflection->getFileName(), 3); - if (!is_file($composerProjectPath . '/composer.json')) { - throw new \PHPStan\ShouldNotHappenException(sprintf('composer.json not found in directory %s', $composerProjectPath)); - } - - $composerSourceLocator = $this->composerJsonAndInstalledJsonSourceLocatorMaker->create($composerProjectPath); - if ($composerSourceLocator === null) { - throw new \PHPStan\ShouldNotHappenException('Could not create composer source locator'); + $classLoaders = ClassLoader::getRegisteredLoaders(); + $classLoaderReflection = new ReflectionClass(ClassLoader::class); + $locators = []; + if ($classLoaderReflection->hasProperty('vendorDir')) { + $vendorDirProperty = $classLoaderReflection->getProperty('vendorDir'); + $vendorDirProperty->setAccessible(true); + foreach ($classLoaders as $classLoader) { + $composerProjectPath = dirname($vendorDirProperty->getValue($classLoader)); + if (!is_file($composerProjectPath . '/composer.json')) { + continue; + } + + $composerSourceLocator = $this->composerJsonAndInstalledJsonSourceLocatorMaker->create($composerProjectPath); + if ($composerSourceLocator === null) { + continue; + } + $locators[] = $composerSourceLocator; + } } - $locators = [ - $composerSourceLocator, - ]; - $astLocator = new Locator($this->phpParser, function (): FunctionReflector { - return $this->container->getService('testCaseFunctionReflector'); - }); - $astPhp8Locator = new Locator($this->php8Parser, function (): FunctionReflector { - return $this->container->getService('betterReflectionFunctionReflector'); - }); + $astLocator = new Locator($this->phpParser); + $astPhp8Locator = new Locator($this->php8Parser); $locators[] = new PhpInternalSourceLocator($astPhp8Locator, $this->phpstormStubsSourceStubber); $locators[] = $this->autoloadSourceLocator; diff --git a/src/Testing/TypeInferenceTestCase.php b/src/Testing/TypeInferenceTestCase.php index 6e5fdea242..7b4d63da20 100644 --- a/src/Testing/TypeInferenceTestCase.php +++ b/src/Testing/TypeInferenceTestCase.php @@ -9,86 +9,61 @@ use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Analyser\ScopeContext; -use PHPStan\Broker\AnonymousClassNameHelper; -use PHPStan\Broker\Broker; -use PHPStan\Cache\Cache; use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; use PHPStan\File\FileHelper; -use PHPStan\File\SimpleRelativePathHelper; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocInheritanceResolver; -use PHPStan\PhpDoc\PhpDocNodeResolver; -use PHPStan\PhpDoc\PhpDocStringResolver; -use PHPStan\Reflection\ReflectionProvider\DirectReflectionProviderProvider; +use PHPStan\PhpDoc\StubPhpDocProvider; use PHPStan\TrinaryLogic; -use PHPStan\Type\DynamicMethodReturnTypeExtension; -use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\VerbosityLevel; +use ReflectionProperty; +use function array_map; +use function array_merge; +use function count; +use function is_string; +use function sprintf; /** @api */ -abstract class TypeInferenceTestCase extends \PHPStan\Testing\TestCase +abstract class TypeInferenceTestCase extends PHPStanTestCase { - /** @var bool */ - protected $polluteCatchScopeWithTryAssignments = true; - /** - * @param string $file - * @param callable(\PhpParser\Node, \PHPStan\Analyser\Scope): void $callback - * @param DynamicMethodReturnTypeExtension[] $dynamicMethodReturnTypeExtensions - * @param DynamicStaticMethodReturnTypeExtension[] $dynamicStaticMethodReturnTypeExtensions - * @param \PHPStan\Type\MethodTypeSpecifyingExtension[] $methodTypeSpecifyingExtensions - * @param \PHPStan\Type\StaticMethodTypeSpecifyingExtension[] $staticMethodTypeSpecifyingExtensions + * @param callable(Node , Scope ): void $callback * @param string[] $dynamicConstantNames */ public function processFile( string $file, callable $callback, - array $dynamicMethodReturnTypeExtensions = [], - array $dynamicStaticMethodReturnTypeExtensions = [], - array $methodTypeSpecifyingExtensions = [], - array $staticMethodTypeSpecifyingExtensions = [], - array $dynamicConstantNames = [] + array $dynamicConstantNames = [], ): void { - $phpDocStringResolver = self::getContainer()->getByType(PhpDocStringResolver::class); - $phpDocNodeResolver = self::getContainer()->getByType(PhpDocNodeResolver::class); - - $printer = new \PhpParser\PrettyPrinter\Standard(); - $broker = $this->createBroker($dynamicMethodReturnTypeExtensions, $dynamicStaticMethodReturnTypeExtensions); - Broker::registerInstance($broker); - $typeSpecifier = $this->createTypeSpecifier($printer, $broker, $methodTypeSpecifyingExtensions, $staticMethodTypeSpecifyingExtensions); - $currentWorkingDirectory = $this->getCurrentWorkingDirectory(); - $fileHelper = new FileHelper($currentWorkingDirectory); - $fileTypeMapper = new FileTypeMapper(new DirectReflectionProviderProvider($broker), $this->getParser(), $phpDocStringResolver, $phpDocNodeResolver, $this->createMock(Cache::class), new AnonymousClassNameHelper($fileHelper, new SimpleRelativePathHelper($currentWorkingDirectory))); - $phpDocInheritanceResolver = new PhpDocInheritanceResolver($fileTypeMapper); + $reflectionProvider = $this->createReflectionProvider(); + $typeSpecifier = self::getContainer()->getService('typeSpecifier'); + $fileHelper = self::getContainer()->getByType(FileHelper::class); $resolver = new NodeScopeResolver( - $broker, - self::getReflectors()[0], + $reflectionProvider, + self::getReflector(), $this->getClassReflectionExtensionRegistryProvider(), $this->getParser(), - $fileTypeMapper, + self::getContainer()->getByType(FileTypeMapper::class), + self::getContainer()->getByType(StubPhpDocProvider::class), self::getContainer()->getByType(PhpVersion::class), - $phpDocInheritanceResolver, - $fileHelper, + self::getContainer()->getByType(PhpDocInheritanceResolver::class), + self::getContainer()->getByType(FileHelper::class), $typeSpecifier, self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class), true, - $this->polluteCatchScopeWithTryAssignments, true, $this->getEarlyTerminatingMethodCalls(), $this->getEarlyTerminatingFunctionCalls(), true, - true ); - $resolver->setAnalysedFiles(array_map(static function (string $file) use ($fileHelper): string { - return $fileHelper->normalizePath($file); - }, array_merge([$file], $this->getAdditionalAnalysedFiles()))); + $resolver->setAnalysedFiles(array_map(static fn (string $file): string => $fileHelper->normalizePath($file), array_merge([$file], $this->getAdditionalAnalysedFiles()))); - $scopeFactory = $this->createScopeFactory($broker, $typeSpecifier); + $scopeFactory = $this->createScopeFactory($reflectionProvider, $typeSpecifier); if (count($dynamicConstantNames) > 0) { - $reflectionProperty = new \ReflectionProperty(DirectScopeFactory::class, 'dynamicConstantNames'); + $reflectionProperty = new ReflectionProperty(DirectScopeFactory::class, 'dynamicConstantNames'); $reflectionProperty->setAccessible(true); $reflectionProperty->setValue($scopeFactory, $dynamicConstantNames); } @@ -97,20 +72,18 @@ public function processFile( $resolver->processNodes( $this->getParser()->parseFile($file), $scope, - $callback + $callback, ); } /** * @api - * @param string $assertType - * @param string $file * @param mixed ...$args */ public function assertFileAsserts( string $assertType, string $file, - ...$args + ...$args, ): void { if ($assertType === 'type') { @@ -121,7 +94,7 @@ public function assertFileAsserts( $this->assertSame( $expected, $actual, - sprintf('Expected type %s, got type %s in %s on line %d.', $expected, $actual, $file, $args[2]) + sprintf('Expected type %s, got type %s in %s on line %d.', $expected, $actual, $file, $args[2]), ); } elseif ($assertType === 'variableCertainty') { $expectedCertainty = $args[0]; @@ -129,14 +102,13 @@ public function assertFileAsserts( $variableName = $args[2]; $this->assertTrue( $expectedCertainty->equals($actualCertainty), - sprintf('Expected %s, actual certainty of variable $%s is %s', $expectedCertainty->describe(), $variableName, $actualCertainty->describe()) + sprintf('Expected %s, actual certainty of variable $%s is %s', $expectedCertainty->describe(), $variableName, $actualCertainty->describe()), ); } } /** * @api - * @param string $file * @return array */ public function gatherAssertTypes(string $file): array @@ -154,16 +126,16 @@ public function gatherAssertTypes(string $file): array $functionName = $nameNode->toString(); if ($functionName === 'PHPStan\\Testing\\assertType') { - $expectedType = $scope->getType($node->args[0]->value); - $actualType = $scope->getType($node->args[1]->value); + $expectedType = $scope->getType($node->getArgs()[0]->value); + $actualType = $scope->getType($node->getArgs()[1]->value); $assert = ['type', $file, $expectedType, $actualType, $node->getLine()]; } elseif ($functionName === 'PHPStan\\Testing\\assertNativeType') { $nativeScope = $scope->doNotTreatPhpDocTypesAsCertain(); - $expectedType = $nativeScope->getNativeType($node->args[0]->value); - $actualType = $nativeScope->getNativeType($node->args[1]->value); + $expectedType = $nativeScope->getNativeType($node->getArgs()[0]->value); + $actualType = $nativeScope->getNativeType($node->getArgs()[1]->value); $assert = ['type', $file, $expectedType, $actualType, $node->getLine()]; } elseif ($functionName === 'PHPStan\\Testing\\assertVariableCertainty') { - $certainty = $node->args[0]->value; + $certainty = $node->getArgs()[0]->value; if (!$certainty instanceof StaticCall) { $this->fail(sprintf('First argument of %s() must be TrinaryLogic call', $functionName)); } @@ -181,7 +153,7 @@ public function gatherAssertTypes(string $file): array // @phpstan-ignore-next-line $expectedertaintyValue = TrinaryLogic::{$certainty->name->toString()}(); - $variable = $node->args[1]->value; + $variable = $node->getArgs()[1]->value; if (!$variable instanceof Node\Expr\Variable) { $this->fail(sprintf('ERROR: Invalid assertVariableCertainty call.')); } @@ -195,11 +167,11 @@ public function gatherAssertTypes(string $file): array return; } - if (count($node->args) !== 2) { + if (count($node->getArgs()) !== 2) { $this->fail(sprintf( 'ERROR: Wrong %s() call on line %d.', $functionName, - $node->getLine() + $node->getLine(), )); } diff --git a/src/Testing/functions.php b/src/Testing/functions.php index e9ce4f7cd6..01fc96b04c 100644 --- a/src/Testing/functions.php +++ b/src/Testing/functions.php @@ -8,7 +8,6 @@ * Asserts the static type of a value. * * @phpstan-pure - * @param string $type * @param mixed $value * @return mixed */ @@ -24,7 +23,6 @@ function assertType(string $type, $value) // phpcs:ignore * method/function parameter phpDocs. * * @phpstan-pure - * @param string $type * @param mixed $value * @return mixed */ @@ -35,7 +33,6 @@ function assertNativeType(string $type, $value) // phpcs:ignore /** * @phpstan-pure - * @param TrinaryLogic $certainty * @param mixed $variable * @return mixed */ diff --git a/src/TrinaryLogic.php b/src/TrinaryLogic.php index 90a02fd870..8776d14519 100644 --- a/src/TrinaryLogic.php +++ b/src/TrinaryLogic.php @@ -4,6 +4,9 @@ use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; +use function array_column; +use function max; +use function min; /** * @api @@ -16,14 +19,11 @@ class TrinaryLogic private const MAYBE = 0; private const NO = -1; - private int $value; - /** @var self[] */ private static array $registry = []; - private function __construct(int $value) + private function __construct(private int $value) { - $this->value = $value; } public static function createYes(): self @@ -48,7 +48,7 @@ public static function createFromBoolean(bool $value): self private static function create(int $value): self { - self::$registry[$value] = self::$registry[$value] ?? new self($value); + self::$registry[$value] ??= new self($value); return self::$registry[$value]; } @@ -92,6 +92,9 @@ public function or(self ...$operands): self public static function extremeIdentity(self ...$operands): self { + if ($operands === []) { + throw new ShouldNotHappenException(); + } $operandValues = array_column($operands, 'value'); $min = min($operandValues); $max = max($operandValues); @@ -100,6 +103,9 @@ public static function extremeIdentity(self ...$operands): self public static function maxMin(self ...$operands): self { + if ($operands === []) { + throw new ShouldNotHappenException(); + } $operandValues = array_column($operands, 'value'); return self::create(max($operandValues) > 0 ? max($operandValues) : min($operandValues)); } @@ -138,7 +144,6 @@ public function describe(): string /** * @param mixed[] $properties - * @return self */ public static function __set_state(array $properties): self { diff --git a/src/Type/Accessory/AccessoryLiteralStringType.php b/src/Type/Accessory/AccessoryLiteralStringType.php new file mode 100644 index 0000000000..1c8a9bc81d --- /dev/null +++ b/src/Type/Accessory/AccessoryLiteralStringType.php @@ -0,0 +1,200 @@ +isAcceptedBy($this, $strictTypes); + } + + return $type->isLiteralString(); + } + + public function isSuperTypeOf(Type $type): TrinaryLogic + { + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + if ($this->equals($type)) { + return TrinaryLogic::createYes(); + } + + return $type->isLiteralString(); + } + + public function isSubTypeOf(Type $otherType): TrinaryLogic + { + if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { + return $otherType->isSuperTypeOf($this); + } + + return $otherType->isLiteralString() + ->and($otherType instanceof self ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe()); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + { + return $this->isSubTypeOf($acceptingType); + } + + public function equals(Type $type): bool + { + return $type instanceof self; + } + + public function describe(VerbosityLevel $level): string + { + return 'literal-string'; + } + + public function isOffsetAccessible(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function hasOffsetValueType(Type $offsetType): TrinaryLogic + { + return (new IntegerType())->isSuperTypeOf($offsetType)->and(TrinaryLogic::createMaybe()); + } + + public function getOffsetValueType(Type $offsetType): Type + { + if ($this->hasOffsetValueType($offsetType)->no()) { + return new ErrorType(); + } + + return new StringType(); + } + + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + { + return $this; + } + + public function unsetOffset(Type $offsetType): Type + { + return new ErrorType(); + } + + public function isArray(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function toNumber(): Type + { + return new ErrorType(); + } + + public function toInteger(): Type + { + return new IntegerType(); + } + + public function toFloat(): Type + { + return new FloatType(); + } + + public function toString(): Type + { + return $this; + } + + public function toBoolean(): BooleanType + { + return new BooleanType(); + } + + public function toArray(): Type + { + return new ConstantArrayType( + [new ConstantIntegerType(0)], + [$this], + 1, + ); + } + + public function isString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isNumericString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function traverse(callable $cb): Type + { + return $this; + } + + public function generalize(GeneralizePrecision $precision): Type + { + return new StringType(); + } + + public static function __set_state(array $properties): Type + { + return new self(); + } + +} diff --git a/src/Type/Accessory/AccessoryNonEmptyStringType.php b/src/Type/Accessory/AccessoryNonEmptyStringType.php new file mode 100644 index 0000000000..7e17a4f524 --- /dev/null +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -0,0 +1,196 @@ +isAcceptedBy($this, $strictTypes); + } + + return $type->isNonEmptyString(); + } + + public function isSuperTypeOf(Type $type): TrinaryLogic + { + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + if ($this->equals($type)) { + return TrinaryLogic::createYes(); + } + + return $type->isNonEmptyString(); + } + + public function isSubTypeOf(Type $otherType): TrinaryLogic + { + if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { + return $otherType->isSuperTypeOf($this); + } + + return $otherType->isNonEmptyString() + ->and($otherType instanceof self ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe()); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + { + return $this->isSubTypeOf($acceptingType); + } + + public function equals(Type $type): bool + { + return $type instanceof self; + } + + public function describe(VerbosityLevel $level): string + { + return 'non-empty-string'; + } + + public function isOffsetAccessible(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function hasOffsetValueType(Type $offsetType): TrinaryLogic + { + return (new IntegerType())->isSuperTypeOf($offsetType)->and(TrinaryLogic::createMaybe()); + } + + public function getOffsetValueType(Type $offsetType): Type + { + if ($this->hasOffsetValueType($offsetType)->no()) { + return new ErrorType(); + } + + if ((new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes()) { + return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]); + } + + return new StringType(); + } + + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + { + return $this; + } + + public function unsetOffset(Type $offsetType): Type + { + return new ErrorType(); + } + + public function isArray(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function toNumber(): Type + { + return new ErrorType(); + } + + public function toInteger(): Type + { + return new IntegerType(); + } + + public function toFloat(): Type + { + return new FloatType(); + } + + public function toString(): Type + { + return $this; + } + + public function toArray(): Type + { + return new ConstantArrayType( + [new ConstantIntegerType(0)], + [$this], + 1, + ); + } + + public function isString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isNumericString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function traverse(callable $cb): Type + { + return $this; + } + + public function generalize(GeneralizePrecision $precision): Type + { + return new StringType(); + } + + public static function __set_state(array $properties): Type + { + return new self(); + } + +} diff --git a/src/Type/Accessory/AccessoryNumericStringType.php b/src/Type/Accessory/AccessoryNumericStringType.php index 0c9fbf324f..28438a2a37 100644 --- a/src/Type/Accessory/AccessoryNumericStringType.php +++ b/src/Type/Accessory/AccessoryNumericStringType.php @@ -4,11 +4,11 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\CompoundType; -use PHPStan\Type\CompoundTypeHelper; 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\IntegerType; use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; @@ -16,10 +16,12 @@ use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\UndecidedBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; use PHPStan\Type\UnionType; +use PHPStan\Type\VerbosityLevel; class AccessoryNumericStringType implements CompoundType, AccessoryType { @@ -30,6 +32,7 @@ class AccessoryNumericStringType implements CompoundType, AccessoryType use UndecidedBooleanTypeTrait; use UndecidedComparisonCompoundTypeTrait; use NonGenericTypeTrait; + use NonRemoveableTypeTrait; /** @api */ public function __construct() @@ -44,7 +47,7 @@ public function getReferencedClasses(): array public function accepts(Type $type, bool $strictTypes): TrinaryLogic { if ($type instanceof CompoundType) { - return CompoundTypeHelper::accepts($type, $this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } return $type->isNumericString(); @@ -52,6 +55,10 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic public function isSuperTypeOf(Type $type): TrinaryLogic { + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + if ($this->equals($type)) { return TrinaryLogic::createYes(); } @@ -79,9 +86,9 @@ public function equals(Type $type): bool return $type instanceof self; } - public function describe(\PHPStan\Type\VerbosityLevel $level): string + public function describe(VerbosityLevel $level): string { - return 'numeric'; + return 'numeric-string'; } public function isOffsetAccessible(): TrinaryLogic @@ -108,6 +115,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } + public function unsetOffset(Type $offsetType): Type + { + return new ErrorType(); + } + public function isArray(): TrinaryLogic { return TrinaryLogic::createNo(); @@ -141,20 +153,40 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1 + 1, ); } + public function isString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function isNumericString(): TrinaryLogic { return TrinaryLogic::createYes(); } + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function traverse(callable $cb): Type { return $this; } + public function generalize(GeneralizePrecision $precision): Type + { + return new StringType(); + } + public static function __set_state(array $properties): Type { return new self(); diff --git a/src/Type/Accessory/HasMethodType.php b/src/Type/Accessory/HasMethodType.php index 9ad20b04f1..e63285cdeb 100644 --- a/src/Type/Accessory/HasMethodType.php +++ b/src/Type/Accessory/HasMethodType.php @@ -11,11 +11,16 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\CompoundType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\ObjectTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; use PHPStan\Type\UnionType; +use PHPStan\Type\VerbosityLevel; +use function sprintf; +use function strtolower; class HasMethodType implements AccessoryType, CompoundType { @@ -23,13 +28,12 @@ class HasMethodType implements AccessoryType, CompoundType use ObjectTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonCompoundTypeTrait; - - private string $methodName; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; /** @api */ - public function __construct(string $methodName) + public function __construct(private string $methodName) { - $this->methodName = $methodName; } public function getReferencedClasses(): array @@ -78,7 +82,7 @@ public function equals(Type $type): bool && $this->getCanonicalMethodName() === $type->getCanonicalMethodName(); } - public function describe(\PHPStan\Type\VerbosityLevel $level): string + public function describe(VerbosityLevel $level): string { return sprintf('hasMethod(%s)', $this->methodName); } @@ -104,9 +108,7 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce $method, $method->getDeclaringClass(), false, - static function (Type $type): Type { - return $type; - } + static fn (Type $type): Type => $type, ); } diff --git a/src/Type/Accessory/HasOffsetType.php b/src/Type/Accessory/HasOffsetType.php index 3ceee34cdf..cbe2da116f 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -4,7 +4,6 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\CompoundType; -use PHPStan\Type\CompoundTypeHelper; use PHPStan\Type\ConstantScalarType; use PHPStan\Type\ErrorType; use PHPStan\Type\IntersectionType; @@ -12,11 +11,15 @@ use PHPStan\Type\Traits\MaybeCallableTypeTrait; use PHPStan\Type\Traits\MaybeIterableTypeTrait; use PHPStan\Type\Traits\MaybeObjectTypeTrait; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\TruthyBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; use PHPStan\Type\UnionType; +use PHPStan\Type\VerbosityLevel; +use function sprintf; class HasOffsetType implements CompoundType, AccessoryType { @@ -27,13 +30,12 @@ class HasOffsetType implements CompoundType, AccessoryType use TruthyBooleanTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonCompoundTypeTrait; - - private \PHPStan\Type\Type $offsetType; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; /** @api */ - public function __construct(Type $offsetType) + public function __construct(private Type $offsetType) { - $this->offsetType = $offsetType; } public function getOffsetType(): Type @@ -49,7 +51,7 @@ public function getReferencedClasses(): array public function accepts(Type $type, bool $strictTypes): TrinaryLogic { if ($type instanceof CompoundType) { - return CompoundTypeHelper::accepts($type, $this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } return $type->isOffsetAccessible() @@ -87,7 +89,7 @@ public function equals(Type $type): bool && $this->offsetType->equals($type->offsetType); } - public function describe(\PHPStan\Type\VerbosityLevel $level): string + public function describe(VerbosityLevel $level): string { return sprintf('hasOffset(%s)', $this->offsetType->describe($level)); } @@ -116,6 +118,14 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } + public function unsetOffset(Type $offsetType): Type + { + if ($this->offsetType->isSuperTypeOf($offsetType)->yes()) { + return new ErrorType(); + } + return $this; + } + public function isIterableAtLeastOnce(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -126,11 +136,26 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isNumericString(): TrinaryLogic { return TrinaryLogic::createMaybe(); } + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function toNumber(): Type { return new ErrorType(); diff --git a/src/Type/Accessory/HasPropertyType.php b/src/Type/Accessory/HasPropertyType.php index b669c3a085..1d65057878 100644 --- a/src/Type/Accessory/HasPropertyType.php +++ b/src/Type/Accessory/HasPropertyType.php @@ -7,11 +7,15 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\CompoundType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\ObjectTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; use PHPStan\Type\UnionType; +use PHPStan\Type\VerbosityLevel; +use function sprintf; class HasPropertyType implements AccessoryType, CompoundType { @@ -19,13 +23,12 @@ class HasPropertyType implements AccessoryType, CompoundType use ObjectTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonCompoundTypeTrait; - - private string $propertyName; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; /** @api */ - public function __construct(string $propertyName) + public function __construct(private string $propertyName) { - $this->propertyName = $propertyName; } /** @@ -77,7 +80,7 @@ public function equals(Type $type): bool && $this->propertyName === $type->propertyName; } - public function describe(\PHPStan\Type\VerbosityLevel $level): string + public function describe(VerbosityLevel $level): string { return sprintf('hasProperty(%s)', $this->propertyName); } diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index f2edcb180c..9ed064559c 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -4,17 +4,21 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\CompoundType; -use PHPStan\Type\CompoundTypeHelper; +use PHPStan\Type\Constant\ConstantFloatType; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\ErrorType; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\Traits\MaybeCallableTypeTrait; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\TruthyBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; use PHPStan\Type\Type; use PHPStan\Type\UnionType; +use PHPStan\Type\VerbosityLevel; class NonEmptyArrayType implements CompoundType, AccessoryType { @@ -24,6 +28,8 @@ class NonEmptyArrayType implements CompoundType, AccessoryType use TruthyBooleanTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonCompoundTypeTrait; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; /** @api */ public function __construct() @@ -38,7 +44,7 @@ public function getReferencedClasses(): array public function accepts(Type $type, bool $strictTypes): TrinaryLogic { if ($type instanceof CompoundType) { - return CompoundTypeHelper::accepts($type, $this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } return $type->isArray() @@ -80,9 +86,9 @@ public function equals(Type $type): bool return $type instanceof self; } - public function describe(\PHPStan\Type\VerbosityLevel $level): string + public function describe(VerbosityLevel $level): string { - return 'nonEmpty'; + return 'non-empty-array'; } public function isOffsetAccessible(): TrinaryLogic @@ -105,6 +111,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } + public function unsetOffset(Type $offsetType): Type + { + return new ErrorType(); + } + public function isIterable(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -130,11 +141,26 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNumericString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function toNumber(): Type { return new ErrorType(); @@ -142,12 +168,12 @@ public function toNumber(): Type public function toInteger(): Type { - return new ErrorType(); + return new ConstantIntegerType(1); } public function toFloat(): Type { - return new ErrorType(); + return new ConstantFloatType(1.0); } public function toString(): Type diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index f298d805b5..8d7d323b20 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -3,10 +3,14 @@ namespace PHPStan\Type; use PHPStan\Reflection\ClassMemberAccessAnswerer; +use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\TrivialParametersAcceptor; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\HasOffsetType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\TemplateMixedType; @@ -14,9 +18,15 @@ use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Traits\MaybeCallableTypeTrait; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; use PHPStan\Type\Traits\UndecidedBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; +use function array_merge; +use function is_float; +use function is_int; +use function key; +use function sprintf; /** @api */ class ArrayType implements Type @@ -26,19 +36,17 @@ class ArrayType implements Type use NonObjectTypeTrait; use UndecidedBooleanTypeTrait; use UndecidedComparisonTypeTrait; + use NonGeneralizableTypeTrait; - private \PHPStan\Type\Type $keyType; - - private \PHPStan\Type\Type $itemType; + private Type $keyType; /** @api */ - public function __construct(Type $keyType, Type $itemType) + public function __construct(Type $keyType, private Type $itemType) { if ($keyType->describe(VerbosityLevel::value()) === '(int|string)') { $keyType = new MixedType(); } $this->keyType = $keyType; - $this->itemType = $itemType; } public function getKeyType(): Type @@ -58,14 +66,14 @@ public function getReferencedClasses(): array { return array_merge( $this->keyType->getReferencedClasses(), - $this->getItemType()->getReferencedClasses() + $this->getItemType()->getReferencedClasses(), ); } public function accepts(Type $type, bool $strictTypes): TrinaryLogic { if ($type instanceof CompoundType) { - return CompoundTypeHelper::accepts($type, $this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } if ($type instanceof ConstantArrayType) { @@ -140,13 +148,13 @@ function () use ($level, $isMixedKeyType, $isMixedItemType): string { } return sprintf('array<%s, %s>', $this->keyType->describe($level), $this->itemType->describe($level)); - } + }, ); } public function generalizeValues(): self { - return new self($this->keyType, TypeUtils::generalizeType($this->itemType)); + return new self($this->keyType, $this->itemType->generalize(GeneralizePrecision::lessSpecific())); } public function getKeysArray(): self @@ -175,6 +183,9 @@ public function getIterableKeyType(): Type if ($keyType instanceof MixedType && !$keyType instanceof TemplateMixedType) { return new BenevolentUnionType([new IntegerType(), new StringType()]); } + if ($keyType instanceof StrictMixedType) { + return new BenevolentUnionType([new IntegerType(), new StringType()]); + } return $keyType; } @@ -189,11 +200,26 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNumericString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isOffsetAccessible(): TrinaryLogic { return TrinaryLogic::createYes(); @@ -232,23 +258,27 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return TypeCombinator::intersect(new self( TypeCombinator::union($this->keyType, self::castToArrayKeyType($offsetType)), - $unionValues ? TypeCombinator::union($this->itemType, $valueType) : $valueType + $unionValues ? TypeCombinator::union($this->itemType, $valueType) : $valueType, ), new NonEmptyArrayType()); } + public function unsetOffset(Type $offsetType): Type + { + return $this; + } + public function isCallable(): TrinaryLogic { return TrinaryLogic::createMaybe()->and((new StringType())->isSuperTypeOf($this->itemType)); } /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] + * @return ParametersAcceptor[] */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { if ($this->isCallable()->no()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return [new TrivialParametersAcceptor()]; @@ -266,12 +296,18 @@ public function toString(): Type public function toInteger(): Type { - return new ErrorType(); + return TypeCombinator::union( + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ); } public function toFloat(): Type { - return new ErrorType(); + return TypeCombinator::union( + new ConstantFloatType(0.0), + new ConstantFloatType(1.0), + ); } public function toArray(): Type @@ -292,8 +328,12 @@ public static function castToArrayKeyType(Type $offsetType): Type } if ($offsetType instanceof ConstantScalarType) { + $keyValue = $offsetType->getValue(); + if (is_float($keyValue)) { + $keyValue = (int) $keyValue; + } /** @var int|string $offsetValue */ - $offsetValue = key([$offsetType->getValue() => null]); + $offsetValue = key([$keyValue => null]); return is_int($offsetValue) ? new ConstantIntegerType($offsetValue) : new ConstantStringType($offsetValue); } @@ -301,11 +341,15 @@ public static function castToArrayKeyType(Type $offsetType): Type return $offsetType; } - if ($offsetType instanceof FloatType || $offsetType instanceof BooleanType || $offsetType->isNumericString()->yes()) { + if ($offsetType instanceof BooleanType) { + return new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]); + } + + if ($offsetType instanceof FloatType || $offsetType->isNumericString()->yes()) { return new IntegerType(); } - if ($offsetType instanceof StringType) { + if ($offsetType instanceof StringType || $offsetType->isNonEmptyString()->yes()) { return $offsetType; } @@ -323,25 +367,6 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap return $receivedType->inferTemplateTypesOn($this); } - if ($receivedType instanceof ConstantArrayType && count($receivedType->getKeyTypes()) === 0) { - $keyType = $this->getKeyType(); - $typeMap = TemplateTypeMap::createEmpty(); - if ($keyType instanceof TemplateType) { - $typeMap = new TemplateTypeMap([ - $keyType->getName() => $keyType->getBound(), - ]); - } - - $itemType = $this->getItemType(); - if ($itemType instanceof TemplateType) { - $typeMap = $typeMap->union(new TemplateTypeMap([ - $itemType->getName() => $itemType->getBound(), - ])); - } - - return $typeMap; - } - if ($receivedType->isArray()->yes()) { $keyTypeMap = $this->getKeyType()->inferTemplateTypes($receivedType->getIterableKeyType()); $itemTypeMap = $this->getItemType()->inferTemplateTypes($receivedType->getIterableValueType()); @@ -371,7 +396,7 @@ public function getReferencedTemplateTypes(TemplateTypeVariance $positionVarianc return array_merge( $this->getKeyType()->getReferencedTemplateTypes($keyVariance), - $this->getItemType()->getReferencedTemplateTypes($itemVariance) + $this->getItemType()->getReferencedTemplateTypes($itemVariance), ); } @@ -381,21 +406,41 @@ public function traverse(callable $cb): Type $itemType = $cb($this->itemType); if ($keyType !== $this->keyType || $itemType !== $this->itemType) { + if ($keyType instanceof NeverType && $itemType instanceof NeverType) { + return new ConstantArrayType([], []); + } + return new self($keyType, $itemType); } return $this; } + public function tryRemove(Type $typeToRemove): ?Type + { + if ($typeToRemove instanceof ConstantArrayType && $typeToRemove->isIterableAtLeastOnce()->no()) { + return TypeCombinator::intersect($this, new NonEmptyArrayType()); + } + + if ($typeToRemove instanceof NonEmptyArrayType) { + return new ConstantArrayType([], []); + } + + if ($this instanceof ConstantArrayType && $typeToRemove instanceof HasOffsetType) { + return $this->unsetOffset($typeToRemove->getOffsetType()); + } + + return null; + } + /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { return new self( $properties['keyType'], - $properties['itemType'] + $properties['itemType'], ); } diff --git a/src/Type/BenevolentUnionType.php b/src/Type/BenevolentUnionType.php index 92ac19a4f1..384488ff21 100644 --- a/src/Type/BenevolentUnionType.php +++ b/src/Type/BenevolentUnionType.php @@ -4,11 +4,22 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; +use function array_map; +use function count; /** @api */ class BenevolentUnionType extends UnionType { + /** + * @api + * @param Type[] $types + */ + public function __construct(array $types) + { + parent::__construct($types); + } + public function describe(VerbosityLevel $level): string { return '(' . parent::describe($level) . ')'; @@ -92,7 +103,6 @@ public function traverse(callable $cb): Type /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { diff --git a/src/Type/BooleanType.php b/src/Type/BooleanType.php index 5911facc94..5521b2bacb 100644 --- a/src/Type/BooleanType.php +++ b/src/Type/BooleanType.php @@ -2,15 +2,17 @@ namespace PHPStan\Type; -use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Traits\NonCallableTypeTrait; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; +use PHPStan\Type\Traits\NonOffsetAccessibleTypeTrait; use PHPStan\Type\Traits\UndecidedBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; @@ -25,6 +27,8 @@ class BooleanType implements Type use UndecidedBooleanTypeTrait; use UndecidedComparisonTypeTrait; use NonGenericTypeTrait; + use NonOffsetAccessibleTypeTrait; + use NonGeneralizableTypeTrait; /** @api */ public function __construct() @@ -45,7 +49,7 @@ public function toString(): Type { return TypeCombinator::union( new ConstantStringType(''), - new ConstantStringType('1') + new ConstantStringType('1'), ); } @@ -53,7 +57,7 @@ public function toInteger(): Type { return TypeCombinator::union( new ConstantIntegerType(0), - new ConstantIntegerType(1) + new ConstantIntegerType(1), ); } @@ -61,7 +65,7 @@ public function toFloat(): Type { return TypeCombinator::union( new ConstantFloatType(0.0), - new ConstantFloatType(1.0) + new ConstantFloatType(1.0), ); } @@ -70,33 +74,21 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1 + 1, ); } - public function isOffsetAccessible(): TrinaryLogic + public function tryRemove(Type $typeToRemove): ?Type { - return TrinaryLogic::createNo(); - } - - public function hasOffsetValueType(Type $offsetType): TrinaryLogic - { - return TrinaryLogic::createNo(); - } + if ($typeToRemove instanceof ConstantBooleanType) { + return new ConstantBooleanType(!$typeToRemove->getValue()); + } - public function getOffsetValueType(Type $offsetType): Type - { - return new ErrorType(); - } - - public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type - { - return new ErrorType(); + return null; } /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index ddeeb6530d..cf05b87486 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -7,6 +7,7 @@ use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeHelper; @@ -15,8 +16,14 @@ use PHPStan\Type\Traits\MaybeIterableTypeTrait; use PHPStan\Type\Traits\MaybeObjectTypeTrait; use PHPStan\Type\Traits\MaybeOffsetAccessibleTypeTrait; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\TruthyBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use function array_map; +use function array_merge; +use function implode; +use function sprintf; /** @api */ class CallableType implements CompoundType, ParametersAcceptor @@ -27,31 +34,28 @@ class CallableType implements CompoundType, ParametersAcceptor use MaybeOffsetAccessibleTypeTrait; use TruthyBooleanTypeTrait; use UndecidedComparisonCompoundTypeTrait; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; - /** @var array */ + /** @var array */ private array $parameters; private Type $returnType; - private bool $variadic; - private bool $isCommonCallable; /** * @api - * @param array $parameters - * @param Type $returnType - * @param bool $variadic + * @param array $parameters */ public function __construct( ?array $parameters = null, ?Type $returnType = null, - bool $variadic = true + private bool $variadic = true, ) { $this->parameters = $parameters ?? []; $this->returnType = $returnType ?? new MixedType(); - $this->variadic = $variadic; $this->isCommonCallable = $parameters === null && $returnType === null; } @@ -71,7 +75,7 @@ public function getReferencedClasses(): array public function accepts(Type $type, bool $strictTypes): TrinaryLogic { if ($type instanceof CompoundType && !$type instanceof self) { - return CompoundTypeHelper::accepts($type, $this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } return $this->isSuperTypeOfInternal($type, true); @@ -105,7 +109,7 @@ private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): Trina } if ($variantsResult === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $isCallable->and($variantsResult); @@ -134,21 +138,15 @@ public function equals(Type $type): bool public function describe(VerbosityLevel $level): string { return $level->handle( - static function (): string { - return 'callable'; - }, - function () use ($level): string { - return sprintf( - 'callable(%s): %s', - implode(', ', array_map( - static function (ParameterReflection $param) use ($level): string { - return sprintf('%s%s', $param->isVariadic() ? '...' : '', $param->getType()->describe($level)); - }, - $this->getParameters() - )), - $this->returnType->describe($level) - ); - } + static fn (): string => 'callable', + fn (): string => sprintf( + 'callable(%s): %s', + implode(', ', array_map( + static fn (ParameterReflection $param): string => sprintf('%s%s', $param->isVariadic() ? '...' : '', $param->getType()->describe($level)), + $this->getParameters(), + )), + $this->returnType->describe($level), + ), ); } @@ -158,8 +156,7 @@ public function isCallable(): TrinaryLogic } /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] + * @return ParametersAcceptor[] */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { @@ -202,7 +199,7 @@ public function getResolvedTemplateTypeMap(): TemplateTypeMap } /** - * @return array + * @return array */ public function getParameters(): array { @@ -225,7 +222,7 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap return $receivedType->inferTemplateTypesOn($this); } - if ($receivedType->isCallable()->no()) { + if (! $receivedType->isCallable()->yes()) { return TemplateTypeMap::createEmpty(); } @@ -265,7 +262,7 @@ private function inferTemplateTypesOnParametersAcceptor(ParametersAcceptor $para public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array { $references = $this->getReturnType()->getReferencedTemplateTypes( - $positionVariance->compose(TemplateTypeVariance::createCovariant()) + $positionVariance->compose(TemplateTypeVariance::createCovariant()), ); $paramVariance = $positionVariance->compose(TemplateTypeVariance::createContravariant()); @@ -293,14 +290,14 @@ public function traverse(callable $cb): Type $cb($param->getType()), $param->passedByReference(), $param->isVariadic(), - $defaultValue !== null ? $cb($defaultValue) : null + $defaultValue !== null ? $cb($defaultValue) : null, ); }, $this->getParameters()); return new self( $parameters, $cb($this->getReturnType()), - $this->isVariadic() + $this->isVariadic(), ); } @@ -309,11 +306,26 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isNumericString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isCommonCallable(): bool { return $this->isCommonCallable; @@ -321,14 +333,13 @@ public function isCommonCallable(): bool /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { return new self( (bool) $properties['isCommonCallable'] ? null : $properties['parameters'], (bool) $properties['isCommonCallable'] ? null : $properties['returnType'], - $properties['variadic'] + $properties['variadic'], ); } diff --git a/src/Type/CallableTypeHelper.php b/src/Type/CallableTypeHelper.php index 85698a22b3..94e903da5e 100644 --- a/src/Type/CallableTypeHelper.php +++ b/src/Type/CallableTypeHelper.php @@ -11,7 +11,7 @@ class CallableTypeHelper public static function isParametersAcceptorSuperTypeOf( ParametersAcceptor $ours, ParametersAcceptor $theirs, - bool $treatMixedAsAny + bool $treatMixedAsAny, ): TrinaryLogic { $theirParameters = $theirs->getParameters(); diff --git a/src/Type/CircularTypeAliasDefinitionException.php b/src/Type/CircularTypeAliasDefinitionException.php index 62a946a020..ad4bbfa25e 100644 --- a/src/Type/CircularTypeAliasDefinitionException.php +++ b/src/Type/CircularTypeAliasDefinitionException.php @@ -2,7 +2,9 @@ namespace PHPStan\Type; -class CircularTypeAliasDefinitionException extends \Exception +use Exception; + +class CircularTypeAliasDefinitionException extends Exception { } diff --git a/src/Type/CircularTypeAliasErrorType.php b/src/Type/CircularTypeAliasErrorType.php new file mode 100644 index 0000000000..929d3c6aec --- /dev/null +++ b/src/Type/CircularTypeAliasErrorType.php @@ -0,0 +1,9 @@ +hasClass($type->getValue())); + return TrinaryLogic::createFromBoolean($type->isClassString()); } if ($type instanceof self) { @@ -46,8 +44,7 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic public function isSuperTypeOf(Type $type): TrinaryLogic { if ($type instanceof ConstantStringType) { - $broker = Broker::getInstance(); - return TrinaryLogic::createFromBoolean($broker->hasClass($type->getValue())); + return TrinaryLogic::createFromBoolean($type->isClassString()); } if ($type instanceof self) { @@ -65,9 +62,28 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return TrinaryLogic::createNo(); } + public function isString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isNumericString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index 26b2971b61..a28df65cc1 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -2,6 +2,7 @@ namespace PHPStan\Type; +use Closure; use PHPStan\Analyser\OutOfClassScope; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; @@ -21,8 +22,15 @@ use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; +use PHPStan\Type\Traits\NonOffsetAccessibleTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; +use function array_map; +use function array_merge; +use function implode; +use function sprintf; /** @api */ class ClosureType implements TypeWithClassName, ParametersAcceptor @@ -30,32 +38,31 @@ class ClosureType implements TypeWithClassName, ParametersAcceptor use NonGenericTypeTrait; use UndecidedComparisonTypeTrait; + use NonOffsetAccessibleTypeTrait; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; private ObjectType $objectType; - /** @var array */ - private array $parameters; + private TemplateTypeMap $templateTypeMap; - private Type $returnType; - - private bool $variadic; + private TemplateTypeMap $resolvedTemplateTypeMap; /** * @api - * @param array $parameters - * @param Type $returnType - * @param bool $variadic + * @param array $parameters */ public function __construct( - array $parameters, - Type $returnType, - bool $variadic + private array $parameters, + private Type $returnType, + private bool $variadic, + ?TemplateTypeMap $templateTypeMap = null, + ?TemplateTypeMap $resolvedTemplateTypeMap = null, ) { - $this->objectType = new ObjectType(\Closure::class); - $this->parameters = $parameters; - $this->returnType = $returnType; - $this->variadic = $variadic; + $this->objectType = new ObjectType(Closure::class); + $this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty(); + $this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap ?? TemplateTypeMap::createEmpty(); } public function getClassName(): string @@ -89,7 +96,7 @@ public function getReferencedClasses(): array public function accepts(Type $type, bool $strictTypes): TrinaryLogic { if ($type instanceof CompoundType) { - return CompoundTypeHelper::accepts($type, $this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } if (!$type instanceof ClosureType) { @@ -110,13 +117,13 @@ private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): Trina return CallableTypeHelper::isParametersAcceptorSuperTypeOf( $this, $type, - $treatMixedAsAny + $treatMixedAsAny, ); } if ( $type instanceof TypeWithClassName - && $type->getClassName() === \Closure::class + && $type->getClassName() === Closure::class ) { return TrinaryLogic::createMaybe(); } @@ -136,18 +143,12 @@ public function equals(Type $type): bool public function describe(VerbosityLevel $level): string { return $level->handle( - static function (): string { - return 'Closure'; - }, - function () use ($level): string { - return sprintf( - 'Closure(%s): %s', - implode(', ', array_map(static function (ParameterReflection $parameter) use ($level): string { - return sprintf('%s%s', $parameter->isVariadic() ? '...' : '', $parameter->getType()->describe($level)); - }, $this->parameters)), - $this->returnType->describe($level) - ); - } + static fn (): string => 'Closure', + fn (): string => sprintf( + 'Closure(%s): %s', + implode(', ', array_map(static fn (ParameterReflection $parameter): string => sprintf('%s%s', $parameter->isVariadic() ? '...' : '', $parameter->getType()->describe($level)), $this->parameters)), + $this->returnType->describe($level), + ), ); } @@ -191,7 +192,7 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce if ($methodName === 'call') { return new ClosureCallUnresolvedMethodPrototypeReflection( $this->objectType->getUnresolvedMethodPrototype($methodName, $scope), - $this + $this, ); } @@ -233,34 +234,13 @@ public function getIterableValueType(): Type return new ErrorType(); } - public function isOffsetAccessible(): TrinaryLogic - { - return TrinaryLogic::createNo(); - } - - public function hasOffsetValueType(Type $offsetType): TrinaryLogic - { - return TrinaryLogic::createNo(); - } - - public function getOffsetValueType(Type $offsetType): Type - { - return new ErrorType(); - } - - public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type - { - return new ErrorType(); - } - public function isCallable(): TrinaryLogic { return TrinaryLogic::createYes(); } /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] + * @return ParametersAcceptor[] */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { @@ -302,22 +282,22 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1 + 1, ); } public function getTemplateTypeMap(): TemplateTypeMap { - return TemplateTypeMap::createEmpty(); + return $this->templateTypeMap; } public function getResolvedTemplateTypeMap(): TemplateTypeMap { - return TemplateTypeMap::createEmpty(); + return $this->resolvedTemplateTypeMap; } /** - * @return array + * @return array */ public function getParameters(): array { @@ -340,7 +320,7 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap return $receivedType->inferTemplateTypesOn($this); } - if ($receivedType->isCallable()->no()) { + if ($receivedType->isCallable()->no() || ! $receivedType instanceof self) { return TemplateTypeMap::createEmpty(); } @@ -388,11 +368,13 @@ public function traverse(callable $cb): Type $cb($param->getType()), $param->passedByReference(), $param->isVariadic(), - $defaultValue !== null ? $cb($defaultValue) : null + $defaultValue !== null ? $cb($defaultValue) : null, ); }, $this->getParameters()), $cb($this->getReturnType()), - $this->isVariadic() + $this->isVariadic(), + $this->templateTypeMap, + $this->resolvedTemplateTypeMap, ); } @@ -401,21 +383,37 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNumericString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { return new self( $properties['parameters'], $properties['returnType'], - $properties['variadic'] + $properties['variadic'], + $properties['templateTypeMap'], + $properties['resolvedTemplateTypeMap'], ); } diff --git a/src/Type/CommentHelper.php b/src/Type/CommentHelper.php deleted file mode 100644 index 3f9e3501ae..0000000000 --- a/src/Type/CommentHelper.php +++ /dev/null @@ -1,21 +0,0 @@ -getDocComment(); - if ($phpDoc !== null) { - return $phpDoc->getText(); - } - - return null; - } - -} diff --git a/src/Type/CompoundTypeHelper.php b/src/Type/CompoundTypeHelper.php deleted file mode 100644 index a86e36eb5e..0000000000 --- a/src/Type/CompoundTypeHelper.php +++ /dev/null @@ -1,15 +0,0 @@ -isAcceptedBy($otherType, $strictTypes); - } - -} diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 7b0a6eb5ab..761390ce05 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -2,10 +2,12 @@ namespace PHPStan\Type\Constant; -use PHPStan\Broker\Broker; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\InaccessibleMethod; +use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\Reflection\TrivialParametersAcceptor; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; @@ -13,6 +15,7 @@ use PHPStan\Type\CompoundType; 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; @@ -22,13 +25,28 @@ use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function array_keys; +use function array_map; +use function array_merge; +use function array_pop; +use function array_slice; use function array_unique; +use function array_values; +use function assert; +use function count; +use function implode; +use function in_array; +use function is_string; +use function max; +use function pow; +use function sprintf; +use function strpos; /** * @api @@ -38,17 +56,6 @@ class ConstantArrayType extends ArrayType implements ConstantType private const DESCRIBE_LIMIT = 8; - /** @var array */ - private array $keyTypes; - - /** @var array */ - private array $valueTypes; - - private int $nextAutoIndex; - - /** @var int[] */ - private array $optionalKeys; - /** @var self[]|null */ private ?array $allArrays = null; @@ -56,27 +63,21 @@ class ConstantArrayType extends ArrayType implements ConstantType * @api * @param array $keyTypes * @param array $valueTypes - * @param int $nextAutoIndex * @param int[] $optionalKeys */ public function __construct( - array $keyTypes, - array $valueTypes, - int $nextAutoIndex = 0, - array $optionalKeys = [] + private array $keyTypes, + private array $valueTypes, + private int $nextAutoIndex = 0, + private array $optionalKeys = [], ) { assert(count($keyTypes) === count($valueTypes)); parent::__construct( - count($keyTypes) > 0 ? TypeCombinator::union(...$keyTypes) : new NeverType(), - count($valueTypes) > 0 ? TypeCombinator::union(...$valueTypes) : new NeverType() + count($keyTypes) > 0 ? TypeCombinator::union(...$keyTypes) : new NeverType(true), + count($valueTypes) > 0 ? TypeCombinator::union(...$valueTypes) : new NeverType(true), ); - - $this->keyTypes = $keyTypes; - $this->valueTypes = $valueTypes; - $this->nextAutoIndex = $nextAutoIndex; - $this->optionalKeys = $optionalKeys; } public function isEmpty(): bool @@ -133,7 +134,7 @@ public function getAllArrays(): array $array = $builder->getArray(); if (!$array instanceof ConstantArrayType) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $arrays[] = $array; @@ -273,7 +274,7 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return $result->and( $this->getKeyType()->isSuperTypeOf($type->getKeyType()), - $this->getItemType()->isSuperTypeOf($type->getItemType()) + $this->getItemType()->isSuperTypeOf($type->getItemType()), ); } @@ -322,14 +323,13 @@ public function isCallable(): TrinaryLogic } /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] + * @return ParametersAcceptor[] */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { $typeAndMethodName = $this->findTypeAndMethodName(); if ($typeAndMethodName === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ($typeAndMethodName->isUnknown() || !$typeAndMethodName->getCertainty()->yes()) { @@ -367,14 +367,14 @@ public function findTypeAndMethodName(): ?ConstantArrayTypeAndMethod } if ($classOrObject instanceof ConstantStringType) { - $broker = Broker::getInstance(); - if (!$broker->hasClass($classOrObject->getValue())) { + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + if (!$reflectionProvider->hasClass($classOrObject->getValue())) { return ConstantArrayTypeAndMethod::createUnknown(); } - $type = new ObjectType($broker->getClass($classOrObject->getValue())->getName()); + $type = new ObjectType($reflectionProvider->getClass($classOrObject->getValue())->getName()); } elseif ($classOrObject instanceof GenericClassStringType) { $type = $classOrObject->getGenericType(); - } elseif ((new \PHPStan\Type\ObjectWithoutClassType())->isSuperTypeOf($classOrObject)->yes()) { + } elseif ((new ObjectWithoutClassType())->isSuperTypeOf($classOrObject)->yes()) { $type = $classOrObject; } else { return ConstantArrayTypeAndMethod::createUnknown(); @@ -498,7 +498,7 @@ public function unsetOffset(Type $offsetType): Type $arrays[] = new self($tmp->keyTypes, $tmp->valueTypes, $tmp->nextAutoIndex, array_keys($tmp->keyTypes)); } - return TypeUtils::generalizeType(TypeCombinator::union(...$arrays)); + return TypeCombinator::union(...$arrays)->generalize(GeneralizePrecision::moreSpecific()); } public function isIterableAtLeastOnce(): TrinaryLogic @@ -539,7 +539,7 @@ public function removeLast(): self $keyTypes, $valueTypes, $nextAutoindex, - array_values($optionalKeys) + array_values($optionalKeys), ); } @@ -599,7 +599,7 @@ public function slice(int $offset, ?int $limit, bool $preserveKeys = false): sel $keyTypes, $valueTypes, (int) $nextAutoIndex, - [] + [], ); } @@ -608,15 +608,29 @@ public function toBoolean(): BooleanType return $this->count()->toBoolean(); } - public function generalize(): Type + public function toInteger(): Type + { + return $this->toBoolean()->toInteger(); + } + + public function toFloat(): Type + { + return $this->toBoolean()->toFloat(); + } + + public function generalize(GeneralizePrecision $precision): Type { if (count($this->keyTypes) === 0) { return $this; } + if ($precision->isTemplateArgument()) { + return $this->traverse(static fn (Type $type) => $type->generalize($precision)); + } + $arrayType = new ArrayType( - TypeUtils::generalizeType($this->getKeyType()), - TypeUtils::generalizeType($this->getItemType()) + $this->getKeyType()->generalize($precision), + $this->getItemType()->generalize($precision), ); if (count($this->keyTypes) > count($this->optionalKeys)) { @@ -633,7 +647,7 @@ public function generalizeValues(): ArrayType { $valueTypes = []; foreach ($this->valueTypes as $valueType) { - $valueTypes[] = TypeUtils::generalizeType($valueType); + $valueTypes[] = $valueType->generalize(GeneralizePrecision::lessSpecific()); } return new self($this->keyTypes, $valueTypes, $this->nextAutoIndex, $this->optionalKeys); @@ -717,7 +731,16 @@ public function describe(VerbosityLevel $level): string $exportValuesOnly = false; } - $items[] = sprintf('%s%s => %s', $isOptional ? '?' : '', var_export($keyType->getValue(), true), $valueType->describe($level)); + $keyDescription = $keyType->getValue(); + if (is_string($keyDescription)) { + if (strpos($keyDescription, '"') !== false) { + $keyDescription = sprintf('\'%s\'', $keyDescription); + } elseif (strpos($keyDescription, '\'') !== false) { + $keyDescription = sprintf('"%s"', $keyDescription); + } + } + + $items[] = sprintf('%s%s: %s', $keyDescription, $isOptional ? '?' : '', $valueType->describe($level)); $values[] = $valueType->describe($level); } @@ -729,21 +752,15 @@ public function describe(VerbosityLevel $level): string } return sprintf( - 'array(%s%s)', + 'array{%s%s}', implode(', ', $exportValuesOnly ? $values : $items), - $append + $append, ); }; return $level->handle( - function () use ($level): string { - return parent::describe($level); - }, - static function () use ($describeValue): string { - return $describeValue(true); - }, - static function () use ($describeValue): string { - return $describeValue(false); - } + fn (): string => parent::describe($level), + static fn (): string => $describeValue(true), + static fn (): string => $describeValue(false), ); } @@ -861,7 +878,6 @@ public function mergeWith(self $otherArray): self /** * @param ConstantIntegerType|ConstantStringType $otherKeyType - * @return int|null */ private function getKeyIndex($otherKeyType): ?int { @@ -898,7 +914,6 @@ public function makeOffsetRequired(Type $offsetType): self /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { diff --git a/src/Type/Constant/ConstantArrayTypeAndMethod.php b/src/Type/Constant/ConstantArrayTypeAndMethod.php index 3bc1809ea8..e5bd1caf7b 100644 --- a/src/Type/Constant/ConstantArrayTypeAndMethod.php +++ b/src/Type/Constant/ConstantArrayTypeAndMethod.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Constant; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; @@ -9,31 +10,22 @@ class ConstantArrayTypeAndMethod { - private ?\PHPStan\Type\Type $type; - - private ?string $method; - - private TrinaryLogic $certainty; - private function __construct( - ?Type $type, - ?string $method, - TrinaryLogic $certainty + private ?Type $type, + private ?string $method, + private TrinaryLogic $certainty, ) { - $this->type = $type; - $this->method = $method; - $this->certainty = $certainty; } public static function createConcrete( Type $type, string $method, - TrinaryLogic $certainty + TrinaryLogic $certainty, ): self { if ($certainty->no()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return new self($type, $method, $certainty); } @@ -51,7 +43,7 @@ public function isUnknown(): bool public function getType(): Type { if ($this->type === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $this->type; @@ -60,7 +52,7 @@ public function getType(): Type public function getMethod(): string { if ($this->method === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $this->method; diff --git a/src/Type/Constant/ConstantArrayTypeBuilder.php b/src/Type/Constant/ConstantArrayTypeBuilder.php index 0cbc15dae7..6af191026b 100644 --- a/src/Type/Constant/ConstantArrayTypeBuilder.php +++ b/src/Type/Constant/ConstantArrayTypeBuilder.php @@ -2,47 +2,39 @@ namespace PHPStan\Type\Constant; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use function array_filter; +use function array_values; +use function count; +use function is_float; +use function max; +use function range; /** @api */ class ConstantArrayTypeBuilder { - /** @var array */ - private array $keyTypes; - - /** @var array */ - private array $valueTypes; - - /** @var array */ - private array $optionalKeys; - - private int $nextAutoIndex; + public const ARRAY_COUNT_LIMIT = 256; private bool $degradeToGeneralArray = false; /** - * @param array $keyTypes + * @param array $keyTypes * @param array $valueTypes * @param array $optionalKeys - * @param int $nextAutoIndex */ private function __construct( - array $keyTypes, - array $valueTypes, - int $nextAutoIndex, - array $optionalKeys + private array $keyTypes, + private array $valueTypes, + private int $nextAutoIndex, + private array $optionalKeys, ) { - $this->keyTypes = $keyTypes; - $this->valueTypes = $valueTypes; - $this->nextAutoIndex = $nextAutoIndex; - $this->optionalKeys = $optionalKeys; } public static function createEmpty(): self @@ -52,12 +44,18 @@ public static function createEmpty(): self public static function createFromConstantArray(ConstantArrayType $startArrayType): self { - return new self( + $builder = new self( $startArrayType->getKeyTypes(), $startArrayType->getValueTypes(), $startArrayType->getNextAutoIndex(), - $startArrayType->getOptionalKeys() + $startArrayType->getOptionalKeys(), ); + + if (count($startArrayType->getKeyTypes()) > self::ARRAY_COUNT_LIMIT) { + $builder->degradeToGeneralArray(); + } + + return $builder; } public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $optional = false): void @@ -68,40 +66,102 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt $offsetType = ArrayType::castToArrayKeyType($offsetType); } - if ( - !$this->degradeToGeneralArray - && ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType) - ) { - /** @var ConstantIntegerType|ConstantStringType $keyType */ - foreach ($this->keyTypes as $i => $keyType) { - if ($keyType->getValue() === $offsetType->getValue()) { - $this->valueTypes[$i] = $valueType; - $this->optionalKeys = array_values(array_filter($this->optionalKeys, static function (int $index) use ($i): bool { - return $index !== $i; - })); - return; + if (!$this->degradeToGeneralArray) { + if ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType) { + /** @var ConstantIntegerType|ConstantStringType $keyType */ + foreach ($this->keyTypes as $i => $keyType) { + if ($keyType->getValue() === $offsetType->getValue()) { + $this->valueTypes[$i] = $valueType; + $this->optionalKeys = array_values(array_filter($this->optionalKeys, static fn (int $index): bool => $index !== $i)); + return; + } + } + + $this->keyTypes[] = $offsetType; + $this->valueTypes[] = $valueType; + + if ($optional) { + $this->optionalKeys[] = count($this->keyTypes) - 1; + } + + /** @var int|float $newNextAutoIndex */ + $newNextAutoIndex = $offsetType instanceof ConstantIntegerType + ? max($this->nextAutoIndex, $offsetType->getValue() + 1) + : $this->nextAutoIndex; + if (!is_float($newNextAutoIndex)) { + $this->nextAutoIndex = $newNextAutoIndex; } - } - $this->keyTypes[] = $offsetType; - $this->valueTypes[] = $valueType; + if (count($this->keyTypes) > self::ARRAY_COUNT_LIMIT) { + $this->degradeToGeneralArray = true; + } - if ($optional) { - $this->optionalKeys[] = count($this->keyTypes) - 1; + return; } - /** @var int|float $newNextAutoIndex */ - $newNextAutoIndex = $offsetType instanceof ConstantIntegerType - ? max($this->nextAutoIndex, $offsetType->getValue() + 1) - : $this->nextAutoIndex; - if (!is_float($newNextAutoIndex)) { - $this->nextAutoIndex = $newNextAutoIndex; + $scalarTypes = TypeUtils::getConstantScalars($offsetType); + if (count($scalarTypes) === 0) { + $integerRanges = TypeUtils::getIntegerRanges($offsetType); + if (count($integerRanges) > 0) { + foreach ($integerRanges as $integerRange) { + if ($integerRange->getMin() === null) { + break; + } + if ($integerRange->getMax() === null) { + break; + } + + $rangeLength = $integerRange->getMax() - $integerRange->getMin(); + if ($rangeLength >= self::ARRAY_COUNT_LIMIT) { + $scalarTypes = []; + break; + } + + foreach (range($integerRange->getMin(), $integerRange->getMax()) as $rangeValue) { + $scalarTypes[] = new ConstantIntegerType($rangeValue); + } + } + } + } + if (count($scalarTypes) > 0 && count($scalarTypes) < self::ARRAY_COUNT_LIMIT) { + $match = true; + $valueTypes = $this->valueTypes; + foreach ($scalarTypes as $scalarType) { + $scalarOffsetType = ArrayType::castToArrayKeyType($scalarType); + if (!$scalarOffsetType instanceof ConstantIntegerType && !$scalarOffsetType instanceof ConstantStringType) { + throw new ShouldNotHappenException(); + } + $offsetMatch = false; + + /** @var ConstantIntegerType|ConstantStringType $keyType */ + foreach ($this->keyTypes as $i => $keyType) { + if ($keyType->getValue() !== $scalarOffsetType->getValue()) { + continue; + } + + $valueTypes[$i] = TypeCombinator::union($valueTypes[$i], $valueType); + $offsetMatch = true; + } + + if ($offsetMatch) { + continue; + } + + $match = false; + } + + if ($match) { + $this->valueTypes = $valueTypes; + return; + } } - return; } - $this->keyTypes[] = TypeUtils::generalizeType($offsetType); + $this->keyTypes[] = $offsetType; $this->valueTypes[] = $valueType; + if ($optional) { + $this->optionalKeys[] = count($this->keyTypes) - 1; + } $this->degradeToGeneralArray = true; } @@ -125,7 +185,7 @@ public function getArray(): Type $array = new ArrayType( TypeCombinator::union(...$this->keyTypes), - TypeCombinator::union(...$this->valueTypes) + TypeCombinator::union(...$this->valueTypes), ); if (count($this->optionalKeys) < $keyTypesCount) { diff --git a/src/Type/Constant/ConstantBooleanType.php b/src/Type/Constant/ConstantBooleanType.php index 30d156eac1..0317f30667 100644 --- a/src/Type/Constant/ConstantBooleanType.php +++ b/src/Type/Constant/ConstantBooleanType.php @@ -17,13 +17,10 @@ class ConstantBooleanType extends BooleanType implements ConstantScalarType use ConstantScalarTypeTrait; - private bool $value; - /** @api */ - public function __construct(bool $value) + public function __construct(private bool $value) { parent::__construct(); - $this->value = $value; } public function getValue(): bool @@ -95,7 +92,6 @@ public function toFloat(): Type /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { diff --git a/src/Type/Constant/ConstantFloatType.php b/src/Type/Constant/ConstantFloatType.php index 55bb56cd1e..2953a60d0a 100644 --- a/src/Type/Constant/ConstantFloatType.php +++ b/src/Type/Constant/ConstantFloatType.php @@ -10,6 +10,7 @@ use PHPStan\Type\Traits\ConstantScalarTypeTrait; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function strpos; /** @api */ class ConstantFloatType extends FloatType implements ConstantScalarType @@ -19,13 +20,10 @@ class ConstantFloatType extends FloatType implements ConstantScalarType use ConstantScalarToBooleanTrait; use ConstantNumericComparisonTypeTrait; - private float $value; - /** @api */ - public function __construct(float $value) + public function __construct(private float $value) { parent::__construct(); - $this->value = $value; } public function getValue(): float @@ -36,9 +34,7 @@ public function getValue(): float public function describe(VerbosityLevel $level): string { return $level->handle( - static function (): string { - return 'float'; - }, + static fn (): string => 'float', function (): string { $formatted = (string) $this->value; if (strpos($formatted, '.') === false) { @@ -46,7 +42,7 @@ function (): string { } return $formatted; - } + }, ); } @@ -87,7 +83,6 @@ public function toInteger(): Type /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { diff --git a/src/Type/Constant/ConstantIntegerType.php b/src/Type/Constant/ConstantIntegerType.php index 5f5c57eddc..9fb760b96e 100644 --- a/src/Type/Constant/ConstantIntegerType.php +++ b/src/Type/Constant/ConstantIntegerType.php @@ -11,6 +11,7 @@ use PHPStan\Type\Traits\ConstantScalarTypeTrait; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** @api */ class ConstantIntegerType extends IntegerType implements ConstantScalarType @@ -20,13 +21,10 @@ class ConstantIntegerType extends IntegerType implements ConstantScalarType use ConstantScalarToBooleanTrait; use ConstantNumericComparisonTypeTrait; - private int $value; - /** @api */ - public function __construct(int $value) + public function __construct(private int $value) { parent::__construct(); - $this->value = $value; } public function getValue(): int @@ -65,12 +63,8 @@ public function isSuperTypeOf(Type $type): TrinaryLogic public function describe(VerbosityLevel $level): string { return $level->handle( - static function (): string { - return 'int'; - }, - function (): string { - return sprintf('%s', $this->value); - } + static fn (): string => 'int', + fn (): string => sprintf('%s', $this->value), ); } @@ -86,7 +80,6 @@ public function toString(): Type /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { diff --git a/src/Type/Constant/ConstantStringType.php b/src/Type/Constant/ConstantStringType.php index ceec668f56..1407a7f66e 100644 --- a/src/Type/Constant/ConstantStringType.php +++ b/src/Type/Constant/ConstantStringType.php @@ -2,19 +2,27 @@ namespace PHPStan\Type\Constant; +use Nette\Utils\RegexpException; +use Nette\Utils\Strings; use PhpParser\Node\Name; -use PHPStan\Broker\Broker; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\InaccessibleMethod; +use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\Reflection\TrivialParametersAcceptor; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\ClassStringType; use PHPStan\Type\CompoundType; use PHPStan\Type\ConstantScalarType; use PHPStan\Type\ErrorType; +use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\IntegerRangeType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; @@ -24,6 +32,11 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; +use function is_float; +use function is_numeric; +use function strlen; +use function substr; +use function var_export; /** @api */ class ConstantStringType extends StringType implements ConstantScalarType @@ -34,16 +47,10 @@ class ConstantStringType extends StringType implements ConstantScalarType use ConstantScalarTypeTrait; use ConstantScalarToBooleanTrait; - private string $value; - - private bool $isClassString; - /** @api */ - public function __construct(string $value, bool $isClassString = false) + public function __construct(private string $value, private bool $isClassString = false) { parent::__construct(); - $this->value = $value; - $this->isClassString = $isClassString; } public function getValue(): string @@ -53,34 +60,36 @@ public function getValue(): string public function isClassString(): bool { - return $this->isClassString; + if ($this->isClassString) { + return true; + } + + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + + return $reflectionProvider->hasClass($this->value); } public function describe(VerbosityLevel $level): string { return $level->handle( - static function (): string { - return 'string'; - }, + static fn (): string => 'string', function (): string { if ($this->isClassString) { return var_export($this->value, true); } try { - $truncatedValue = \Nette\Utils\Strings::truncate($this->value, self::DESCRIBE_LIMIT); - } catch (\Nette\Utils\RegexpException $e) { + $truncatedValue = Strings::truncate($this->value, self::DESCRIBE_LIMIT); + } catch (RegexpException) { $truncatedValue = substr($this->value, 0, self::DESCRIBE_LIMIT) . "\u{2026}"; } return var_export( $truncatedValue, - true + true, ); }, - function (): string { - return var_export($this->value, true); - } + fn (): string => var_export($this->value, true), ); } @@ -114,9 +123,7 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return TrinaryLogic::createNo(); } if ($type instanceof ClassStringType) { - $broker = Broker::getInstance(); - - return $broker->hasClass($this->getValue()) ? TrinaryLogic::createMaybe() : TrinaryLogic::createNo(); + return $this->isClassString() ? TrinaryLogic::createMaybe() : TrinaryLogic::createNo(); } if ($type instanceof self) { @@ -140,21 +147,21 @@ public function isCallable(): TrinaryLogic return TrinaryLogic::createNo(); } - $broker = Broker::getInstance(); + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); // 'my_function' - if ($broker->hasFunction(new Name($this->value), null)) { + if ($reflectionProvider->hasFunction(new Name($this->value), null)) { return TrinaryLogic::createYes(); } // 'MyClass::myStaticFunction' - $matches = \Nette\Utils\Strings::match($this->value, '#^([a-zA-Z_\\x7f-\\xff\\\\][a-zA-Z0-9_\\x7f-\\xff\\\\]*)::([a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*)\z#'); + $matches = Strings::match($this->value, '#^([a-zA-Z_\\x7f-\\xff\\\\][a-zA-Z0-9_\\x7f-\\xff\\\\]*)::([a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*)\z#'); if ($matches !== null) { - if (!$broker->hasClass($matches[1])) { + if (!$reflectionProvider->hasClass($matches[1])) { return TrinaryLogic::createMaybe(); } - $classRef = $broker->getClass($matches[1]); + $classRef = $reflectionProvider->getClass($matches[1]); if ($classRef->hasMethod($matches[2])) { return TrinaryLogic::createYes(); } @@ -170,27 +177,26 @@ public function isCallable(): TrinaryLogic } /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] + * @return ParametersAcceptor[] */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { - $broker = Broker::getInstance(); + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); // 'my_function' $functionName = new Name($this->value); - if ($broker->hasFunction($functionName, null)) { - return $broker->getFunction($functionName, null)->getVariants(); + if ($reflectionProvider->hasFunction($functionName, null)) { + return $reflectionProvider->getFunction($functionName, null)->getVariants(); } // 'MyClass::myStaticFunction' - $matches = \Nette\Utils\Strings::match($this->value, '#^([a-zA-Z_\\x7f-\\xff\\\\][a-zA-Z0-9_\\x7f-\\xff\\\\]*)::([a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*)\z#'); + $matches = Strings::match($this->value, '#^([a-zA-Z_\\x7f-\\xff\\\\][a-zA-Z0-9_\\x7f-\\xff\\\\]*)::([a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*)\z#'); if ($matches !== null) { - if (!$broker->hasClass($matches[1])) { + if (!$reflectionProvider->hasClass($matches[1])) { return [new TrivialParametersAcceptor()]; } - $classReflection = $broker->getClass($matches[1]); + $classReflection = $reflectionProvider->getClass($matches[1]); if ($classReflection->hasMethod($matches[2])) { $method = $classReflection->getMethod($matches[2], $scope); if (!$scope->canCallMethod($method)) { @@ -205,7 +211,7 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) } } - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function toNumber(): Type @@ -226,22 +232,17 @@ public function toNumber(): Type public function toInteger(): Type { - $type = $this->toNumber(); - if ($type instanceof ErrorType) { - return $type; - } - - return $type->toInteger(); + return new ConstantIntegerType((int) $this->value); } public function toFloat(): Type { - $type = $this->toNumber(); - if ($type instanceof ErrorType) { - return $type; - } + return new ConstantFloatType((float) $this->value); + } - return $type->toFloat(); + public function isString(): TrinaryLogic + { + return TrinaryLogic::createYes(); } public function isNumericString(): TrinaryLogic @@ -249,11 +250,21 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createFromBoolean(is_numeric($this->getValue())); } + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->getValue() !== ''); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function hasOffsetValueType(Type $offsetType): TrinaryLogic { if ($offsetType instanceof ConstantIntegerType) { return TrinaryLogic::createFromBoolean( - $offsetType->getValue() < strlen($this->value) + $offsetType->getValue() < strlen($this->value), ); } @@ -284,7 +295,15 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni && $valueStringType instanceof ConstantStringType ) { $value = $this->value; - $value[$offsetType->getValue()] = $valueStringType->getValue(); + $offsetValue = $offsetType->getValue(); + if ($offsetValue < 0) { + return new ErrorType(); + } + $stringValue = $valueStringType->getValue(); + if (strlen($stringValue) !== 1) { + return new ErrorType(); + } + $value[$offsetValue] = $stringValue; return new self($value); } @@ -297,11 +316,31 @@ public function append(self $otherString): self return new self($this->getValue() . $otherString->getValue()); } - public function generalize(): Type + public function generalize(GeneralizePrecision $precision): Type { if ($this->isClassString) { - return new ClassStringType(); + if ($precision->isMoreSpecific()) { + return new ClassStringType(); + } + + return new StringType(); + } + + if ($this->getValue() !== '' && $precision->isMoreSpecific()) { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + new AccessoryLiteralStringType(), + ]); } + + if ($precision->isMoreSpecific()) { + return new IntersectionType([ + new StringType(), + new AccessoryLiteralStringType(), + ]); + } + return new StringType(); } @@ -366,7 +405,6 @@ public function getGreaterOrEqualType(): Type /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { diff --git a/src/Type/ConstantType.php b/src/Type/ConstantType.php index b4ebf5ca84..0f08d47c81 100644 --- a/src/Type/ConstantType.php +++ b/src/Type/ConstantType.php @@ -6,6 +6,6 @@ interface ConstantType extends Type { - public function generalize(): Type; + public function generalize(GeneralizePrecision $precision): Type; } diff --git a/src/Type/ConstantTypeHelper.php b/src/Type/ConstantTypeHelper.php index f8fdd0699a..d5045351df 100644 --- a/src/Type/ConstantTypeHelper.php +++ b/src/Type/ConstantTypeHelper.php @@ -7,6 +7,18 @@ use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Enum\EnumCaseObjectType; +use UnitEnum; +use function count; +use function function_exists; +use function get_class; +use function is_array; +use function is_bool; +use function is_float; +use function is_int; +use function is_nan; +use function is_object; +use function is_string; /** @api */ class ConstantTypeHelper @@ -20,6 +32,9 @@ public static function getTypeFromValue($value): Type if (is_int($value)) { return new ConstantIntegerType($value); } elseif (is_float($value)) { + if (is_nan($value)) { + return new MixedType(); + } return new ConstantFloatType($value); } elseif (is_bool($value)) { return new ConstantBooleanType($value); @@ -29,10 +44,23 @@ public static function getTypeFromValue($value): Type return new ConstantStringType($value); } elseif (is_array($value)) { $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); + if (count($value) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { + $arrayBuilder->degradeToGeneralArray(); + } foreach ($value as $k => $v) { $arrayBuilder->setOffsetValueType(self::getTypeFromValue($k), self::getTypeFromValue($v)); } return $arrayBuilder->getArray(); + } elseif (is_object($value)) { + $class = get_class($value); + /** phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly.ReferenceViaFullyQualifiedName */ + if (function_exists('enum_exists') && \enum_exists($class)) { + /** @var UnitEnum $value */ + return new EnumCaseObjectType($class, $value->name); + } + /** phpcs:enable */ + + return new ObjectType(get_class($value)); } return new MixedType(); diff --git a/src/Type/DirectTypeAliasResolverProvider.php b/src/Type/DirectTypeAliasResolverProvider.php new file mode 100644 index 0000000000..b64fe8f46d --- /dev/null +++ b/src/Type/DirectTypeAliasResolverProvider.php @@ -0,0 +1,17 @@ +typeAliasResolver; + } + +} diff --git a/src/Type/DynamicFunctionReturnTypeExtension.php b/src/Type/DynamicFunctionReturnTypeExtension.php index ac4750ce53..92941735a3 100644 --- a/src/Type/DynamicFunctionReturnTypeExtension.php +++ b/src/Type/DynamicFunctionReturnTypeExtension.php @@ -12,6 +12,6 @@ interface DynamicFunctionReturnTypeExtension public function isFunctionSupported(FunctionReflection $functionReflection): bool; - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type; + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type; } diff --git a/src/Type/DynamicMethodReturnTypeExtension.php b/src/Type/DynamicMethodReturnTypeExtension.php index 58635acca7..35f5b505ca 100644 --- a/src/Type/DynamicMethodReturnTypeExtension.php +++ b/src/Type/DynamicMethodReturnTypeExtension.php @@ -14,6 +14,6 @@ public function getClass(): string; public function isMethodSupported(MethodReflection $methodReflection): bool; - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type; + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type; } diff --git a/src/Type/DynamicReturnTypeExtensionRegistry.php b/src/Type/DynamicReturnTypeExtensionRegistry.php index 584c078658..b747ba4a71 100644 --- a/src/Type/DynamicReturnTypeExtensionRegistry.php +++ b/src/Type/DynamicReturnTypeExtensionRegistry.php @@ -5,40 +5,28 @@ use PHPStan\Broker\Broker; use PHPStan\Reflection\BrokerAwareExtension; use PHPStan\Reflection\ReflectionProvider; +use function array_merge; class DynamicReturnTypeExtensionRegistry { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - /** @var \PHPStan\Type\DynamicMethodReturnTypeExtension[] */ - private array $dynamicMethodReturnTypeExtensions; - - /** @var \PHPStan\Type\DynamicStaticMethodReturnTypeExtension[] */ - private array $dynamicStaticMethodReturnTypeExtensions; - - /** @var \PHPStan\Type\DynamicFunctionReturnTypeExtension[] */ - private array $dynamicFunctionReturnTypeExtensions; - - /** @var \PHPStan\Type\DynamicMethodReturnTypeExtension[][]|null */ + /** @var DynamicMethodReturnTypeExtension[][]|null */ private ?array $dynamicMethodReturnTypeExtensionsByClass = null; - /** @var \PHPStan\Type\DynamicStaticMethodReturnTypeExtension[][]|null */ + /** @var DynamicStaticMethodReturnTypeExtension[][]|null */ private ?array $dynamicStaticMethodReturnTypeExtensionsByClass = null; /** - * @param \PHPStan\Broker\Broker $broker - * @param ReflectionProvider $reflectionProvider - * @param \PHPStan\Type\DynamicMethodReturnTypeExtension[] $dynamicMethodReturnTypeExtensions - * @param \PHPStan\Type\DynamicStaticMethodReturnTypeExtension[] $dynamicStaticMethodReturnTypeExtensions - * @param \PHPStan\Type\DynamicFunctionReturnTypeExtension[] $dynamicFunctionReturnTypeExtensions + * @param DynamicMethodReturnTypeExtension[] $dynamicMethodReturnTypeExtensions + * @param DynamicStaticMethodReturnTypeExtension[] $dynamicStaticMethodReturnTypeExtensions + * @param DynamicFunctionReturnTypeExtension[] $dynamicFunctionReturnTypeExtensions */ public function __construct( Broker $broker, - ReflectionProvider $reflectionProvider, - array $dynamicMethodReturnTypeExtensions, - array $dynamicStaticMethodReturnTypeExtensions, - array $dynamicFunctionReturnTypeExtensions + private ReflectionProvider $reflectionProvider, + private array $dynamicMethodReturnTypeExtensions, + private array $dynamicStaticMethodReturnTypeExtensions, + private array $dynamicFunctionReturnTypeExtensions, ) { foreach (array_merge($dynamicMethodReturnTypeExtensions, $dynamicStaticMethodReturnTypeExtensions, $dynamicFunctionReturnTypeExtensions) as $extension) { @@ -48,16 +36,10 @@ public function __construct( $extension->setBroker($broker); } - - $this->reflectionProvider = $reflectionProvider; - $this->dynamicMethodReturnTypeExtensions = $dynamicMethodReturnTypeExtensions; - $this->dynamicStaticMethodReturnTypeExtensions = $dynamicStaticMethodReturnTypeExtensions; - $this->dynamicFunctionReturnTypeExtensions = $dynamicFunctionReturnTypeExtensions; } /** - * @param string $className - * @return \PHPStan\Type\DynamicMethodReturnTypeExtension[] + * @return DynamicMethodReturnTypeExtension[] */ public function getDynamicMethodReturnTypeExtensionsForClass(string $className): array { @@ -73,8 +55,7 @@ public function getDynamicMethodReturnTypeExtensionsForClass(string $className): } /** - * @param string $className - * @return \PHPStan\Type\DynamicStaticMethodReturnTypeExtension[] + * @return DynamicStaticMethodReturnTypeExtension[] */ public function getDynamicStaticMethodReturnTypeExtensionsForClass(string $className): array { @@ -90,8 +71,7 @@ public function getDynamicStaticMethodReturnTypeExtensionsForClass(string $class } /** - * @param \PHPStan\Type\DynamicMethodReturnTypeExtension[][]|\PHPStan\Type\DynamicStaticMethodReturnTypeExtension[][] $extensions - * @param string $className + * @param DynamicMethodReturnTypeExtension[][]|DynamicStaticMethodReturnTypeExtension[][] $extensions * @return mixed[] */ private function getDynamicExtensionsForType(array $extensions, string $className): array @@ -114,7 +94,7 @@ private function getDynamicExtensionsForType(array $extensions, string $classNam } /** - * @return \PHPStan\Type\DynamicFunctionReturnTypeExtension[] + * @return DynamicFunctionReturnTypeExtension[] */ public function getDynamicFunctionReturnTypeExtensions(): array { diff --git a/src/Type/DynamicStaticMethodReturnTypeExtension.php b/src/Type/DynamicStaticMethodReturnTypeExtension.php index e2de530f9f..94560039a6 100644 --- a/src/Type/DynamicStaticMethodReturnTypeExtension.php +++ b/src/Type/DynamicStaticMethodReturnTypeExtension.php @@ -14,6 +14,6 @@ public function getClass(): string; public function isStaticMethodSupported(MethodReflection $methodReflection): bool; - public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type; + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type; } diff --git a/src/Type/Enum/EnumCaseObjectType.php b/src/Type/Enum/EnumCaseObjectType.php new file mode 100644 index 0000000000..4181b375d9 --- /dev/null +++ b/src/Type/Enum/EnumCaseObjectType.php @@ -0,0 +1,143 @@ +enumCaseName; + } + + public function describe(VerbosityLevel $level): string + { + $parent = parent::describe($level); + + return sprintf('%s::%s', $parent, $this->enumCaseName); + } + + public function equals(Type $type): bool + { + if (!$type instanceof self) { + return false; + } + + return $this->getClassName() === $type->getClassName() + && $this->enumCaseName === $type->enumCaseName; + } + + public function accepts(Type $type, bool $strictTypes): TrinaryLogic + { + return $this->isSuperTypeOf($type); + } + + public function isSuperTypeOf(Type $type): TrinaryLogic + { + if ($type instanceof self) { + return TrinaryLogic::createFromBoolean( + $this->getClassName() === $type->getClassName() + && $this->enumCaseName === $type->enumCaseName, + ); + } + + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + return $type->isSuperTypeOf($this)->yes() ? TrinaryLogic::createMaybe() : TrinaryLogic::createNo(); + } + + public function subtract(Type $type): Type + { + return $this; + } + + public function getTypeWithoutSubtractedType(): Type + { + return $this; + } + + public function changeSubtractedType(?Type $subtractedType): Type + { + return $this; + } + + public function getSubtractedType(): ?Type + { + return null; + } + + public function tryRemove(Type $typeToRemove): ?Type + { + if ($this->isSuperTypeOf($typeToRemove)->yes()) { + return $this->subtract($typeToRemove); + } + + return null; + } + + public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection + { + $classReflection = $this->getClassReflection(); + if ($classReflection === null) { + return parent::getProperty($propertyName, $scope); + + } + if ($propertyName === 'name') { + return new EnumPropertyReflection($classReflection, new ConstantStringType($this->enumCaseName)); + } + + if ($classReflection->isBackedEnum() && $propertyName === 'value') { + if ($classReflection->hasEnumCase($this->enumCaseName)) { + $enumCase = $classReflection->getEnumCase($this->enumCaseName); + $valueType = $enumCase->getBackingValueType(); + if ($valueType === null) { + throw new ShouldNotHappenException(); + } + + return new EnumPropertyReflection($classReflection, $valueType); + } + } + + return parent::getProperty($propertyName, $scope); + } + + public function generalize(GeneralizePrecision $precision): Type + { + return new parent($this->getClassName(), null, $this->getClassReflection()); + } + + /** + * @param mixed[] $properties + */ + public static function __set_state(array $properties): Type + { + return new self($properties['className'], $properties['enumCaseName'], null); + } + +} diff --git a/src/Type/ErrorType.php b/src/Type/ErrorType.php index 9eb2e2d10a..345465b306 100644 --- a/src/Type/ErrorType.php +++ b/src/Type/ErrorType.php @@ -15,15 +15,9 @@ public function __construct() public function describe(VerbosityLevel $level): string { return $level->handle( - function () use ($level): string { - return parent::describe($level); - }, - function () use ($level): string { - return parent::describe($level); - }, - static function (): string { - return '*ERROR*'; - } + fn (): string => parent::describe($level), + fn (): string => parent::describe($level), + static fn (): string => '*ERROR*', ); } @@ -49,7 +43,6 @@ public function equals(Type $type): bool /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index d9b14ab7a3..bf9f08eb9a 100644 --- a/src/Type/FileTypeMapper.php +++ b/src/Type/FileTypeMapper.php @@ -2,28 +2,47 @@ namespace PHPStan\Type; +use Closure; use PhpParser\Comment\Doc; use PhpParser\Node; use PHPStan\Analyser\NameScope; +use PHPStan\BetterReflection\Util\GetLastDocComment; use PHPStan\Broker\AnonymousClassNameHelper; use PHPStan\Cache\Cache; +use PHPStan\File\FileHelper; use PHPStan\Parser\Parser; -use PHPStan\PhpDoc\NameScopedPhpDocString; +use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocNodeResolver; use PHPStan\PhpDoc\PhpDocStringResolver; use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\PhpDoc\Tag\TemplateTag; -use PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Generic\GenericObjectType; -use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; +use ReflectionClass; use function array_key_exists; -use function file_exists; +use function array_keys; +use function array_map; +use function array_merge; +use function array_pop; +use function array_slice; +use function count; use function filemtime; +use function implode; +use function is_array; +use function is_callable; +use function is_file; +use function ltrim; +use function md5; +use function sprintf; +use function strpos; +use function strtolower; +use function time; +use function trait_exists; class FileTypeMapper { @@ -31,48 +50,33 @@ class FileTypeMapper private const SKIP_NODE = 1; private const POP_TYPE_MAP_STACK = 2; - private ReflectionProviderProvider $reflectionProviderProvider; - - private \PHPStan\Parser\Parser $phpParser; - - private \PHPStan\PhpDoc\PhpDocStringResolver $phpDocStringResolver; - - private \PHPStan\PhpDoc\PhpDocNodeResolver $phpDocNodeResolver; - - private \PHPStan\Cache\Cache $cache; - - private \PHPStan\Broker\AnonymousClassNameHelper $anonymousClassNameHelper; - - /** @var \PHPStan\PhpDoc\NameScopedPhpDocString[][] */ + /** @var NameScope[][] */ private array $memoryCache = []; - /** @var (false|(callable(): \PHPStan\PhpDoc\NameScopedPhpDocString)|\PHPStan\PhpDoc\NameScopedPhpDocString)[][] */ + private int $memoryCacheCount = 0; + + /** @var (false|callable(): NameScope|NameScope)[][] */ private array $inProcess = []; /** @var array */ private array $resolvedPhpDocBlockCache = []; + private int $resolvedPhpDocBlockCacheCount = 0; + /** @var array */ private array $alreadyProcessedDependentFiles = []; - /** @var array */ - private array $docKeys = []; - public function __construct( - ReflectionProviderProvider $reflectionProviderProvider, - Parser $phpParser, - PhpDocStringResolver $phpDocStringResolver, - PhpDocNodeResolver $phpDocNodeResolver, - Cache $cache, - AnonymousClassNameHelper $anonymousClassNameHelper + private ReflectionProviderProvider $reflectionProviderProvider, + private Parser $phpParser, + private PhpDocStringResolver $phpDocStringResolver, + private PhpDocNodeResolver $phpDocNodeResolver, + private Cache $cache, + private AnonymousClassNameHelper $anonymousClassNameHelper, + private PhpVersion $phpVersion, + private FileHelper $fileHelper, ) { - $this->reflectionProviderProvider = $reflectionProviderProvider; - $this->phpParser = $phpParser; - $this->phpDocStringResolver = $phpDocStringResolver; - $this->phpDocNodeResolver = $phpDocNodeResolver; - $this->cache = $cache; - $this->anonymousClassNameHelper = $anonymousClassNameHelper; } /** @api */ @@ -81,75 +85,74 @@ public function getResolvedPhpDoc( ?string $className, ?string $traitName, ?string $functionName, - string $docComment + string $docComment, ): ResolvedPhpDocBlock { + $fileName = $this->fileHelper->normalizePath($fileName); + if ($className === null && $traitName !== null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - $phpDocKey = $this->getPhpDocKey($fileName, $className, $traitName, $functionName, $docComment); + if ($docComment === '') { + return ResolvedPhpDocBlock::createEmpty(); + } + + $nameScopeKey = $this->getNameScopeKey($fileName, $className, $traitName, $functionName); + $phpDocKey = md5(sprintf('%s-%s', $nameScopeKey, $docComment)); if (isset($this->resolvedPhpDocBlockCache[$phpDocKey])) { return $this->resolvedPhpDocBlockCache[$phpDocKey]; } - - $phpDocMap = []; + $nameScopeMap = []; if (!isset($this->inProcess[$fileName])) { - $phpDocMap = $this->getResolvedPhpDocMap($fileName); + $nameScopeMap = $this->getNameScopeMap($fileName); } - if (isset($phpDocMap[$phpDocKey])) { - return $this->createResolvedPhpDocBlock($phpDocKey, $phpDocMap[$phpDocKey], $fileName); + if (isset($nameScopeMap[$nameScopeKey])) { + return $this->createResolvedPhpDocBlock($phpDocKey, $nameScopeMap[$nameScopeKey], $docComment, $fileName); } - if (!isset($this->inProcess[$fileName][$phpDocKey])) { // wrong $fileName due to traits + if (!isset($this->inProcess[$fileName][$nameScopeKey])) { // wrong $fileName due to traits return ResolvedPhpDocBlock::createEmpty(); } - if ($this->inProcess[$fileName][$phpDocKey] === false) { // PHPDoc has cyclic dependency + if ($this->inProcess[$fileName][$nameScopeKey] === false) { // PHPDoc has cyclic dependency return ResolvedPhpDocBlock::createEmpty(); } - if (is_callable($this->inProcess[$fileName][$phpDocKey])) { - $resolveCallback = $this->inProcess[$fileName][$phpDocKey]; - $this->inProcess[$fileName][$phpDocKey] = false; - $this->inProcess[$fileName][$phpDocKey] = $resolveCallback(); + if (is_callable($this->inProcess[$fileName][$nameScopeKey])) { + $resolveCallback = $this->inProcess[$fileName][$nameScopeKey]; + $this->inProcess[$fileName][$nameScopeKey] = false; + $this->inProcess[$fileName][$nameScopeKey] = $resolveCallback(); } - return $this->createResolvedPhpDocBlock($phpDocKey, $this->inProcess[$fileName][$phpDocKey], $fileName); + return $this->createResolvedPhpDocBlock($phpDocKey, $this->inProcess[$fileName][$nameScopeKey], $docComment, $fileName); } - private function createResolvedPhpDocBlock(string $phpDocKey, NameScopedPhpDocString $nameScopedPhpDocString, string $fileName): ResolvedPhpDocBlock + private function createResolvedPhpDocBlock(string $phpDocKey, NameScope $nameScope, string $phpDocString, string $fileName): ResolvedPhpDocBlock { - $phpDocString = $nameScopedPhpDocString->getPhpDocString(); $phpDocNode = $this->resolvePhpDocStringToDocNode($phpDocString); - $nameScope = $nameScopedPhpDocString->getNameScope(); - $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($phpDocNode, $nameScope); - $templateTypeScope = $nameScope->getTemplateTypeScope(); - - if ($templateTypeScope !== null) { - $templateTypeMap = new TemplateTypeMap(array_map(static function (TemplateTag $tag) use ($templateTypeScope): Type { - return TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag); - }, $templateTags)); - $nameScope = $nameScope->withTemplateTypeMap( - new TemplateTypeMap(array_merge( - $nameScope->getTemplateTypeMap()->getTypes(), - $templateTypeMap->getTypes() - )) + if ($this->resolvedPhpDocBlockCacheCount >= 512) { + $this->resolvedPhpDocBlockCache = array_slice( + $this->resolvedPhpDocBlockCache, + 1, + null, + true, ); - $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($phpDocNode, $nameScope); - $templateTypeMap = new TemplateTypeMap(array_map(static function (TemplateTag $tag) use ($templateTypeScope): Type { - return TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag); - }, $templateTags)); - $nameScope = $nameScope->withTemplateTypeMap( - new TemplateTypeMap(array_merge( - $nameScope->getTemplateTypeMap()->getTypes(), - $templateTypeMap->getTypes() - )) - ); - } else { - $templateTypeMap = TemplateTypeMap::createEmpty(); + + $this->resolvedPhpDocBlockCacheCount--; + } + + $templateTypeMap = $nameScope->getTemplateTypeMap(); + $phpDocTemplateTypes = []; + $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($phpDocNode, $nameScope); + foreach (array_keys($templateTags) as $name) { + $templateType = $templateTypeMap->getType($name); + if ($templateType === null) { + continue; + } + $phpDocTemplateTypes[$name] = $templateType; } $this->resolvedPhpDocBlockCache[$phpDocKey] = ResolvedPhpDocBlock::create( @@ -157,70 +160,28 @@ private function createResolvedPhpDocBlock(string $phpDocKey, NameScopedPhpDocSt $phpDocString, $fileName, $nameScope, - $templateTypeMap, + new TemplateTypeMap($phpDocTemplateTypes), $templateTags, - $this->phpDocNodeResolver + $this->phpDocNodeResolver, ); + $this->resolvedPhpDocBlockCacheCount++; return $this->resolvedPhpDocBlockCache[$phpDocKey]; } private function resolvePhpDocStringToDocNode(string $phpDocString): PhpDocNode { - $phpDocParserVersion = 'Version unknown'; - try { - $phpDocParserVersion = \Jean85\PrettyVersions::getVersion('phpstan/phpdoc-parser')->getPrettyVersion(); - } catch (\OutOfBoundsException $e) { - // skip - } - $cacheKey = sprintf('phpdocstring-%s', $phpDocString); - $phpDocNodeSerializedString = $this->cache->load($cacheKey, $phpDocParserVersion); - if ($phpDocNodeSerializedString !== null) { - $unserializeResult = @unserialize($phpDocNodeSerializedString); - if ($unserializeResult === false) { - $error = error_get_last(); - if ($error !== null) { - throw new \PHPStan\ShouldNotHappenException(sprintf('unserialize() error: %s', $error['message'])); - } - - throw new \PHPStan\ShouldNotHappenException('Unknown unserialize() error'); - } - - return $unserializeResult; - } - - $phpDocNode = $this->phpDocStringResolver->resolve($phpDocString); - if ($this->shouldPhpDocNodeBeCachedToDisk($phpDocNode)) { - $this->cache->save($cacheKey, $phpDocParserVersion, serialize($phpDocNode)); - } - - return $phpDocNode; - } - - private function shouldPhpDocNodeBeCachedToDisk(PhpDocNode $phpDocNode): bool - { - foreach ($phpDocNode->getTags() as $phpDocTag) { - if (!$phpDocTag->value instanceof InvalidTagValueNode) { - continue; - } - - return false; - } - - return true; + return $this->phpDocStringResolver->resolve($phpDocString); } /** - * @param string $fileName - * @return \PHPStan\PhpDoc\NameScopedPhpDocString[] + * @return NameScope[] */ - private function getResolvedPhpDocMap(string $fileName): array + private function getNameScopeMap(string $fileName): array { if (!isset($this->memoryCache[$fileName])) { - $cacheKey = sprintf('%s-phpdocstring-v10-function-name-stack', $fileName); - $variableCacheKey = implode(',', array_map(static function (array $file): string { - return sprintf('%s-%d', $file['filename'], $file['modifiedTime']); - }, $this->getCachedDependentFilesWithTimestamps($fileName))); + $cacheKey = sprintf('%s-phpdocstring-v22-trait-bug', $fileName); + $variableCacheKey = sprintf('%s-%s', implode(',', array_map(static fn (array $file): string => sprintf('%s-%d', $file['filename'], $file['modifiedTime']), $this->getCachedDependentFilesWithTimestamps($fileName))), $this->phpVersion->getVersionString()); $map = $this->cache->load($cacheKey, $variableCacheKey); if ($map === null) { @@ -228,53 +189,61 @@ private function getResolvedPhpDocMap(string $fileName): array $this->cache->save($cacheKey, $variableCacheKey, $map); } + if ($this->memoryCacheCount >= 512) { + $this->memoryCache = array_slice( + $this->memoryCache, + 1, + null, + true, + ); + $this->memoryCacheCount--; + } + $this->memoryCache[$fileName] = $map; + $this->memoryCacheCount++; } return $this->memoryCache[$fileName]; } /** - * @param string $fileName - * @return \PHPStan\PhpDoc\NameScopedPhpDocString[] + * @return NameScope[] */ private function createResolvedPhpDocMap(string $fileName): array { - $phpDocMap = $this->createFilePhpDocMap($fileName, null, null); - $resolvedPhpDocMap = []; + $nameScopeMap = $this->createNameScopeMap($fileName, null, null, [], $fileName); + $resolvedNameScopeMap = []; try { - $this->inProcess[$fileName] = $phpDocMap; + $this->inProcess[$fileName] = $nameScopeMap; - foreach ($phpDocMap as $phpDocKey => $resolveCallback) { - $this->inProcess[$fileName][$phpDocKey] = false; - $this->inProcess[$fileName][$phpDocKey] = $data = $resolveCallback(); - $resolvedPhpDocMap[$phpDocKey] = $data; + foreach ($nameScopeMap as $nameScopeKey => $resolveCallback) { + $this->inProcess[$fileName][$nameScopeKey] = false; + $this->inProcess[$fileName][$nameScopeKey] = $data = $resolveCallback(); + $resolvedNameScopeMap[$nameScopeKey] = $data; } } finally { unset($this->inProcess[$fileName]); } - return $resolvedPhpDocMap; + return $resolvedNameScopeMap; } /** - * @param string $fileName - * @param string|null $lookForTrait - * @param string|null $traitUseClass * @param array $traitMethodAliases - * @return (callable(): \PHPStan\PhpDoc\NameScopedPhpDocString)[] + * @return (callable(): NameScope)[] */ - private function createFilePhpDocMap( + private function createNameScopeMap( string $fileName, ?string $lookForTrait, ?string $traitUseClass, - array $traitMethodAliases = [] + array $traitMethodAliases, + string $originalClassFileName, ): array { - /** @var (callable(): \PHPStan\PhpDoc\NameScopedPhpDocString)[] $phpDocMap */ - $phpDocMap = []; + /** @var (callable(): NameScope)[] $nameScopeMap */ + $nameScopeMap = []; /** @var (callable(): TemplateTypeMap)[] $typeMapStack */ $typeMapStack = []; @@ -290,138 +259,133 @@ private function createFilePhpDocMap( } $namespace = null; + $traitFound = false; + /** @var array $functionStack */ $functionStack = []; $uses = []; $this->processNodes( $this->phpParser->parseFile($fileName), - function (\PhpParser\Node $node) use ($fileName, $lookForTrait, $traitMethodAliases, &$phpDocMap, &$classStack, &$typeAliasStack, &$namespace, &$functionStack, &$uses, &$typeMapStack): ?int { - $resolvableTemplateTypes = false; + function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodAliases, $originalClassFileName, &$nameScopeMap, &$classStack, &$typeAliasStack, &$namespace, &$functionStack, &$uses, &$typeMapStack): ?int { if ($node instanceof Node\Stmt\ClassLike) { - if ($lookForTrait !== null) { + 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; } else { if ($node->name === null) { if (!$node instanceof Node\Stmt\Class_) { - throw new \PHPStan\ShouldNotHappenException(); + 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; $typeAliasStack[] = $this->getTypeAliasesMap($node->getDocComment()); $functionStack[] = null; - $resolvableTemplateTypes = true; } - } elseif ($node instanceof Node\Stmt\TraitUse) { - $resolvableTemplateTypes = true; } elseif ($node instanceof Node\Stmt\ClassMethod) { if (array_key_exists($node->name->name, $traitMethodAliases)) { $functionStack[] = $traitMethodAliases[$node->name->name]; } else { $functionStack[] = $node->name->name; } - $resolvableTemplateTypes = true; - } elseif ( - $node instanceof Node\Param - && $node->flags !== 0 - ) { - $resolvableTemplateTypes = true; } elseif ($node instanceof Node\Stmt\Function_) { $functionStack[] = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\'); - $resolvableTemplateTypes = true; - } elseif ($node instanceof Node\Stmt\Property) { - $resolvableTemplateTypes = true; - } elseif ( - !$node instanceof Node\Stmt - && !$node instanceof Node\Expr\Assign - && !$node instanceof Node\Expr\AssignRef - ) { - return null; } - foreach (array_reverse($node->getComments()) as $comment) { - if (!$comment instanceof Doc) { - continue; + $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_) { + $phpDocString = GetLastDocComment::forNode($node); + if ($phpDocString !== '') { + $typeMapStack[] = function () use ($namespace, $uses, $className, $functionName, $phpDocString, $typeMapStack): TemplateTypeMap { + $phpDocNode = $this->resolvePhpDocStringToDocNode($phpDocString); + $typeMapCb = $typeMapStack[count($typeMapStack) - 1] ?? null; + $currentTypeMap = $typeMapCb !== null ? $typeMapCb() : null; + $nameScope = new NameScope($namespace, $uses, $className, $functionName, $currentTypeMap); + $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($phpDocNode, $nameScope); + $templateTypeScope = $nameScope->getTemplateTypeScope(); + if ($templateTypeScope === null) { + throw new ShouldNotHappenException(); + } + $templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $templateTags)); + $nameScope = $nameScope->withTemplateTypeMap($templateTypeMap); + $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($phpDocNode, $nameScope); + $templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $templateTags)); + + return new TemplateTypeMap(array_merge( + $currentTypeMap !== null ? $currentTypeMap->getTypes() : [], + $templateTypeMap->getTypes(), + )); + }; } + } - $phpDocString = $comment->getText(); - $className = $classStack[count($classStack) - 1] ?? null; - $functionName = $functionStack[count($functionStack) - 1] ?? null; - $typeMapCb = $typeMapStack[count($typeMapStack) - 1] ?? null; - $typeAliasesMap = $typeAliasStack[count($typeAliasStack) - 1] ?? []; - - $phpDocKey = $this->getPhpDocKey($fileName, $className, $lookForTrait, $functionName, $phpDocString); - $phpDocMap[$phpDocKey] = static function () use ($phpDocString, $namespace, $uses, $className, $functionName, $typeMapCb, $typeAliasesMap, $resolvableTemplateTypes): NameScopedPhpDocString { - $nameScope = new NameScope( - $namespace, - $uses, - $className, - $functionName, - ($typeMapCb !== null ? $typeMapCb() : TemplateTypeMap::createEmpty())->map(static function (string $name, Type $type) use ($className, $resolvableTemplateTypes): Type { - return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($className, $resolvableTemplateTypes): Type { - if (!$type instanceof TemplateType) { - return $traverse($type); - } - - if (!$resolvableTemplateTypes) { - return $traverse($type->toArgument()); - } - - $scope = $type->getScope(); - - if ($scope->getClassName() === null || $scope->getFunctionName() !== null || $scope->getClassName() !== $className) { - return $traverse($type->toArgument()); - } - - return $traverse($type); - }); - }), - $typeAliasesMap - ); - return new NameScopedPhpDocString($phpDocString, $nameScope); - }; + $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_ + && !$node instanceof Node\Stmt\Declare_ + && !$node instanceof Node\Stmt\DeclareDeclare + && !$node instanceof Node\Stmt\Use_ + && !$node instanceof Node\Stmt\UseUse + && !$node instanceof Node\Stmt\GroupUse + && !$node instanceof Node\Stmt\TraitUse + && !$node instanceof Node\Stmt\TraitUseAdaptation + && !$node instanceof Node\Stmt\InlineHTML + && !($node instanceof Node\Stmt\Expression && $node->expr instanceof Node\Expr\Include_) + && !array_key_exists($nameScopeKey, $nameScopeMap) + ) { + $nameScopeMap[$nameScopeKey] = static fn (): NameScope => new NameScope( + $namespace, + $uses, + $className, + $functionName, + ($typeMapCb !== null ? $typeMapCb() : TemplateTypeMap::createEmpty()), + $typeAliasesMap, + ); + } - if (!($node instanceof Node\Stmt\ClassLike) && !($node instanceof Node\FunctionLike)) { - continue; + if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) { + $phpDocString = GetLastDocComment::forNode($node); + if ($phpDocString !== '') { + return self::POP_TYPE_MAP_STACK; } - $typeMapStack[] = function () use ($fileName, $className, $lookForTrait, $functionName, $phpDocString, $typeMapCb): TemplateTypeMap { - $resolvedPhpDoc = $this->getResolvedPhpDoc( - $fileName, - $className, - $lookForTrait, - $functionName, - $phpDocString - ); - return new TemplateTypeMap(array_merge( - $typeMapCb !== null ? $typeMapCb()->getTypes() : [], - $resolvedPhpDoc->getTemplateTypeMap()->getTypes() - )); - }; - - return self::POP_TYPE_MAP_STACK; + return null; } - if ($node instanceof \PhpParser\Node\Stmt\Namespace_) { + if ($node instanceof Node\Stmt\Namespace_) { $namespace = (string) $node->name; - } elseif ($node instanceof \PhpParser\Node\Stmt\Use_ && $node->type === \PhpParser\Node\Stmt\Use_::TYPE_NORMAL) { + } elseif ($node instanceof Node\Stmt\Use_ && $node->type === Node\Stmt\Use_::TYPE_NORMAL) { foreach ($node->uses as $use) { $uses[strtolower($use->getAlias()->name)] = (string) $use->name; } - } elseif ($node instanceof \PhpParser\Node\Stmt\GroupUse) { + } elseif ($node instanceof Node\Stmt\GroupUse) { $prefix = (string) $node->prefix; foreach ($node->uses as $use) { - if ($node->type !== \PhpParser\Node\Stmt\Use_::TYPE_NORMAL && $use->type !== \PhpParser\Node\Stmt\Use_::TYPE_NORMAL) { + if ($node->type !== Node\Stmt\Use_::TYPE_NORMAL && $use->type !== Node\Stmt\Use_::TYPE_NORMAL) { continue; } @@ -462,28 +426,29 @@ function (\PhpParser\Node $node) use ($fileName, $lookForTrait, $traitMethodAlia if (!$traitReflection->isTrait()) { continue; } - if ($traitReflection->getFileName() === false) { + if ($traitReflection->getFileName() === null) { continue; } - if (!file_exists($traitReflection->getFileName())) { + if (!is_file($traitReflection->getFileName())) { continue; } $className = $classStack[count($classStack) - 1] ?? null; if ($className === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - $traitPhpDocMap = $this->createFilePhpDocMap( + $traitPhpDocMap = $this->createNameScopeMap( $traitReflection->getFileName(), $traitName, $className, - $traitMethodAliases[$traitName] ?? [] + $traitMethodAliases[$traitName] ?? [], + $originalClassFileName, ); $finalTraitPhpDocMap = []; - foreach ($traitPhpDocMap as $phpDocKey => $callback) { - $finalTraitPhpDocMap[$phpDocKey] = function () use ($callback, $traitReflection, $fileName, $className, $lookForTrait, $useDocComment): NameScopedPhpDocString { - /** @var NameScopedPhpDocString $original */ + foreach ($traitPhpDocMap as $nameScopeTraitKey => $callback) { + $finalTraitPhpDocMap[$nameScopeTraitKey] = function () use ($callback, $traitReflection, $fileName, $className, $lookForTrait, $useDocComment): NameScope { + /** @var NameScope $original */ $original = $callback(); if (!$traitReflection->isGeneric()) { return $original; @@ -498,7 +463,7 @@ function (\PhpParser\Node $node) use ($fileName, $lookForTrait, $traitMethodAlia $className, $lookForTrait, null, - $useDocComment + $useDocComment, )->getUsesTags(); foreach ($useTags as $useTag) { $useTagType = $useTag->getType(); @@ -516,52 +481,44 @@ function (\PhpParser\Node $node) use ($fileName, $lookForTrait, $traitMethodAlia } if ($useType === null) { - return new NameScopedPhpDocString( - $original->getPhpDocString(), - $original->getNameScope()->withTemplateTypeMap($traitTemplateTypeMap->resolveToBounds()) - ); + return $original->withTemplateTypeMap($traitTemplateTypeMap->resolveToBounds()); } $transformedTraitTypeMap = $traitReflection->typeMapFromList($useType->getTypes()); - return new NameScopedPhpDocString( - $original->getPhpDocString(), - $original->getNameScope()->withTemplateTypeMap($traitTemplateTypeMap->map(static function (string $name, Type $type) use ($transformedTraitTypeMap): Type { - return TemplateTypeHelper::resolveTemplateTypes($type, $transformedTraitTypeMap); - })) - ); + return $original->withTemplateTypeMap($traitTemplateTypeMap->map(static fn (string $name, Type $type): Type => TemplateTypeHelper::resolveTemplateTypes($type, $transformedTraitTypeMap))); }; } - $phpDocMap = array_merge($phpDocMap, $finalTraitPhpDocMap); + $nameScopeMap = array_merge($nameScopeMap, $finalTraitPhpDocMap); } } return null; }, - static function (\PhpParser\Node $node, $callbackResult) use ($lookForTrait, &$namespace, &$functionStack, &$classStack, &$typeAliasStack, &$uses, &$typeMapStack): void { + static function (Node $node, $callbackResult) use ($lookForTrait, &$namespace, &$functionStack, &$classStack, &$typeAliasStack, &$uses, &$typeMapStack): void { if ($node instanceof Node\Stmt\ClassLike && $lookForTrait === null) { if (count($classStack) === 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } array_pop($classStack); if (count($typeAliasStack) === 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } array_pop($typeAliasStack); if (count($functionStack) === 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } array_pop($functionStack); - } elseif ($node instanceof \PhpParser\Node\Stmt\Namespace_) { + } elseif ($node instanceof Node\Stmt\Namespace_) { $namespace = null; $uses = []; } elseif ($node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) { if (count($functionStack) === 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } array_pop($functionStack); @@ -571,21 +528,20 @@ static function (\PhpParser\Node $node, $callbackResult) use ($lookForTrait, &$n } if (count($typeMapStack) === 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } array_pop($typeMapStack); - } + }, ); if (count($typeMapStack) > 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - return $phpDocMap; + return $nameScopeMap; } /** - * @param Doc|null $docComment * @return array */ private function getTypeAliasesMap(?Doc $docComment): array @@ -610,11 +566,11 @@ private function getTypeAliasesMap(?Doc $docComment): array } /** - * @param \PhpParser\Node[]|\PhpParser\Node|scalar $node - * @param \Closure(\PhpParser\Node $node): mixed $nodeCallback - * @param \Closure(\PhpParser\Node $node, mixed $callbackResult): void $endNodeCallback + * @param Node[]|Node|scalar $node + * @param Closure(Node $node): mixed $nodeCallback + * @param Closure(Node $node, mixed $callbackResult): void $endNodeCallback */ - private function processNodes($node, \Closure $nodeCallback, \Closure $endNodeCallback): void + private function processNodes($node, Closure $nodeCallback, Closure $endNodeCallback): void { if ($node instanceof Node) { $callbackResult = $nodeCallback($node); @@ -633,39 +589,35 @@ private function processNodes($node, \Closure $nodeCallback, \Closure $endNodeCa } } - private function getPhpDocKey( + private function getNameScopeKey( string $file, ?string $class, ?string $trait, ?string $function, - string $docComment ): string { - $cacheKey = md5($docComment); - if (!isset($this->docKeys[$cacheKey])) { - $this->docKeys[$cacheKey] = \Nette\Utils\Strings::replace($docComment, '#\s+#', ' '); + if ($class === null && $trait === null && $function === null) { + return md5(sprintf('%s', $file)); } - $docComment = $this->docKeys[$cacheKey]; - if ($class === null && $trait === null && $function === null) { - return md5(sprintf('%s-%s', $file, $docComment)); + if ($class !== null && strpos($class, 'class@anonymous') !== false) { + throw new ShouldNotHappenException('Wrong anonymous class name, FilTypeMapper should be called with ClassReflection::getName().'); } - return md5(sprintf('%s-%s-%s-%s', $class, $trait, $function, $docComment)); + return md5(sprintf('%s-%s-%s-%s', $file, $class, $trait, $function)); } /** - * @param string $fileName * @return array */ private function getCachedDependentFilesWithTimestamps(string $fileName): array { - $cacheKey = sprintf('dependentFilesTimestamps-%s', $fileName); + $cacheKey = sprintf('dependentFilesTimestamps-%s-v3-filter-ast', $fileName); $fileModifiedTime = filemtime($fileName); if ($fileModifiedTime === false) { $fileModifiedTime = time(); } - $variableCacheKey = sprintf('%d', $fileModifiedTime); + $variableCacheKey = sprintf('%d-%s', $fileModifiedTime, $this->phpVersion->getVersionString()); /** @var array|null $cachedFilesTimestamps */ $cachedFilesTimestamps = $this->cache->load($cacheKey, $variableCacheKey); if ($cachedFilesTimestamps !== null) { @@ -674,7 +626,7 @@ private function getCachedDependentFilesWithTimestamps(string $fileName): array $cachedFilename = $cachedFile['filename']; $cachedTimestamp = $cachedFile['modifiedTime']; - if (!file_exists($cachedFilename)) { + if (!is_file($cachedFilename)) { $useCached = false; break; } @@ -715,7 +667,6 @@ private function getCachedDependentFilesWithTimestamps(string $fileName): array } /** - * @param string $fileName * @return string[] */ private function getDependentFiles(string $fileName): array @@ -738,7 +689,7 @@ function (Node $node) use (&$dependentFiles) { return null; } - if (!$node instanceof Node\Stmt\Class_ && !$node instanceof Node\Stmt\Trait_) { + if (!$node instanceof Node\Stmt\Class_ && !$node instanceof Node\Stmt\Trait_ && !$node instanceof Node\Stmt\Enum_) { return null; } @@ -753,11 +704,11 @@ function (Node $node) use (&$dependentFiles) { continue; } - $traitReflection = new \ReflectionClass($traitName); + $traitReflection = new ReflectionClass($traitName); if ($traitReflection->getFileName() === false) { continue; } - if (!file_exists($traitReflection->getFileName())) { + if (!is_file($traitReflection->getFileName())) { continue; } @@ -770,7 +721,7 @@ function (Node $node) use (&$dependentFiles) { return null; }, static function (): void { - } + }, ); unset($this->alreadyProcessedDependentFiles[$fileName]); diff --git a/src/Type/FloatType.php b/src/Type/FloatType.php index c6ef8824d5..091f427e86 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -7,11 +7,15 @@ use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Traits\NonCallableTypeTrait; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; +use PHPStan\Type\Traits\NonOffsetAccessibleTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\UndecidedBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; +use function get_class; /** @api */ class FloatType implements Type @@ -23,6 +27,9 @@ class FloatType implements Type use UndecidedBooleanTypeTrait; use UndecidedComparisonTypeTrait; use NonGenericTypeTrait; + use NonOffsetAccessibleTypeTrait; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; /** @api */ public function __construct() @@ -104,36 +111,31 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1 + 1, ); } - public function isOffsetAccessible(): TrinaryLogic + public function isArray(): TrinaryLogic { return TrinaryLogic::createNo(); } - public function hasOffsetValueType(Type $offsetType): TrinaryLogic + public function isString(): TrinaryLogic { return TrinaryLogic::createNo(); } - public function getOffsetValueType(Type $offsetType): Type - { - return new ErrorType(); - } - - public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + public function isNumericString(): TrinaryLogic { - return new ErrorType(); + return TrinaryLogic::createNo(); } - public function isArray(): TrinaryLogic + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); } - public function isNumericString(): TrinaryLogic + public function isLiteralString(): TrinaryLogic { return TrinaryLogic::createNo(); } @@ -145,7 +147,6 @@ public function traverse(callable $cb): Type /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { diff --git a/src/Type/GeneralizePrecision.php b/src/Type/GeneralizePrecision.php new file mode 100644 index 0000000000..0cce10ed32 --- /dev/null +++ b/src/Type/GeneralizePrecision.php @@ -0,0 +1,58 @@ +value === self::LESS_SPECIFIC; + } + + public function isMoreSpecific(): bool + { + return $this->value === self::MORE_SPECIFIC; + } + + public function isTemplateArgument(): bool + { + return $this->value === self::TEMPLATE_ARGUMENT; + } + +} diff --git a/src/Type/Generic/GenericClassStringType.php b/src/Type/Generic/GenericClassStringType.php index e91dfb3b95..0568f9c22c 100644 --- a/src/Type/Generic/GenericClassStringType.php +++ b/src/Type/Generic/GenericClassStringType.php @@ -2,7 +2,6 @@ namespace PHPStan\Type\Generic; -use PHPStan\Broker\Broker; use PHPStan\TrinaryLogic; use PHPStan\Type\ClassStringType; use PHPStan\Type\CompoundType; @@ -17,18 +16,16 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** @api */ class GenericClassStringType extends ClassStringType { - private Type $type; - /** @api */ - public function __construct(Type $type) + public function __construct(private Type $type) { parent::__construct(); - $this->type = $type; } public function getReferencedClasses(): array @@ -53,8 +50,7 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic } if ($type instanceof ConstantStringType) { - $broker = Broker::getInstance(); - if (!$broker->hasClass($type->getValue())) { + if (!$type->isClassString()) { return TrinaryLogic::createNo(); } @@ -174,7 +170,6 @@ public function equals(Type $type): bool /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { diff --git a/src/Type/Generic/GenericObjectType.php b/src/Type/Generic/GenericObjectType.php index 8ee2c8a5f1..c7672be92a 100644 --- a/src/Type/Generic/GenericObjectType.php +++ b/src/Type/Generic/GenericObjectType.php @@ -2,13 +2,14 @@ namespace PHPStan\Type\Generic; -use PHPStan\Broker\Broker; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\CompoundType; use PHPStan\Type\ErrorType; @@ -18,30 +19,27 @@ use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function array_map; +use function count; +use function implode; +use function sprintf; /** @api */ class GenericObjectType extends ObjectType { - /** @var array */ - private array $types; - - private ?ClassReflection $classReflection; - /** * @api * @param array $types */ public function __construct( string $mainType, - array $types, + private array $types, ?Type $subtractedType = null, - ?ClassReflection $classReflection = null + private ?ClassReflection $classReflection = null, ) { parent::__construct($mainType, $subtractedType, $classReflection); - $this->types = $types; - $this->classReflection = $classReflection; } public function describe(VerbosityLevel $level): string @@ -49,9 +47,7 @@ public function describe(VerbosityLevel $level): string return sprintf( '%s<%s>', parent::describe($level), - implode(', ', array_map(static function (Type $type) use ($level): string { - return $type->describe($level); - }, $this->types)) + implode(', ', array_map(static fn (Type $type): string => $type->describe($level), $this->types)), ); } @@ -163,7 +159,7 @@ private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): Trinar continue; } if (!$templateType instanceof TemplateType) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $results[] = $templateType->isValidVariance($this->types[$i], $ancestor->types[$i]); @@ -182,12 +178,12 @@ public function getClassReflection(): ?ClassReflection return $this->classReflection; } - $broker = Broker::getInstance(); - if (!$broker->hasClass($this->getClassName())) { + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + if (!$reflectionProvider->hasClass($this->getClassName())) { return null; } - return $this->classReflection = $broker->getClass($this->getClassName())->withTypes($this->types); + return $this->classReflection = $reflectionProvider->getClass($this->getClassName())->withTypes($this->types); } public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection @@ -260,7 +256,7 @@ public function getReferencedTemplateTypes(TemplateTypeVariance $positionVarianc $variance = $positionVariance->compose( isset($typeList[$i]) && $typeList[$i] instanceof TemplateType ? $typeList[$i]->getVariance() - : TemplateTypeVariance::createInvariant() + : TemplateTypeVariance::createInvariant(), ); foreach ($type->getReferencedTemplateTypes($variance) as $reference) { $references[] = $reference; @@ -294,17 +290,14 @@ public function traverse(callable $cb): Type } /** - * @param string $className * @param Type[] $types - * @param Type|null $subtractedType - * @return self */ protected function recreate(string $className, array $types, ?Type $subtractedType): self { return new self( $className, $types, - $subtractedType + $subtractedType, ); } @@ -315,14 +308,13 @@ public function changeSubtractedType(?Type $subtractedType): Type /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { return new self( $properties['className'], $properties['types'], - $properties['subtractedType'] ?? null + $properties['subtractedType'] ?? null, ); } diff --git a/src/Type/Generic/TemplateArrayType.php b/src/Type/Generic/TemplateArrayType.php new file mode 100644 index 0000000000..267411d479 --- /dev/null +++ b/src/Type/Generic/TemplateArrayType.php @@ -0,0 +1,54 @@ + */ + use TemplateTypeTrait; + use UndecidedComparisonCompoundTypeTrait; + + public function __construct( + TemplateTypeScope $scope, + TemplateTypeStrategy $templateTypeStrategy, + TemplateTypeVariance $templateTypeVariance, + string $name, + ArrayType $bound, + ) + { + parent::__construct($bound->getKeyType(), $bound->getItemType()); + $this->scope = $scope; + $this->strategy = $templateTypeStrategy; + $this->variance = $templateTypeVariance; + $this->name = $name; + $this->bound = $bound; + } + + public function traverse(callable $cb): Type + { + $newBound = $cb($this->getBound()); + if ($this->getBound() !== $newBound && $newBound instanceof ArrayType) { + return new self( + $this->scope, + $this->strategy, + $this->variance, + $this->name, + $newBound, + ); + } + + return $this; + } + + protected function shouldGeneralizeInferredType(): bool + { + return false; + } + +} diff --git a/src/Type/Generic/TemplateBenevolentUnionType.php b/src/Type/Generic/TemplateBenevolentUnionType.php index 94e867a15b..9ce83db602 100644 --- a/src/Type/Generic/TemplateBenevolentUnionType.php +++ b/src/Type/Generic/TemplateBenevolentUnionType.php @@ -17,7 +17,7 @@ public function __construct( TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, - BenevolentUnionType $bound + BenevolentUnionType $bound, ) { parent::__construct($bound->getTypes()); @@ -37,7 +37,7 @@ public function withTypes(array $types): self $this->strategy, $this->variance, $this->name, - new BenevolentUnionType($types) + new BenevolentUnionType($types), ); } @@ -50,7 +50,7 @@ public function traverse(callable $cb): Type $this->strategy, $this->variance, $this->name, - $newBound + $newBound, ); } diff --git a/src/Type/Generic/TemplateBooleanType.php b/src/Type/Generic/TemplateBooleanType.php new file mode 100644 index 0000000000..54d55b7987 --- /dev/null +++ b/src/Type/Generic/TemplateBooleanType.php @@ -0,0 +1,54 @@ + */ + use TemplateTypeTrait; + use UndecidedComparisonCompoundTypeTrait; + + public function __construct( + TemplateTypeScope $scope, + TemplateTypeStrategy $templateTypeStrategy, + TemplateTypeVariance $templateTypeVariance, + string $name, + BooleanType $bound, + ) + { + parent::__construct(); + $this->scope = $scope; + $this->strategy = $templateTypeStrategy; + $this->variance = $templateTypeVariance; + $this->name = $name; + $this->bound = $bound; + } + + public function traverse(callable $cb): Type + { + $newBound = $cb($this->getBound()); + if ($this->getBound() !== $newBound && $newBound instanceof BooleanType) { + return new self( + $this->scope, + $this->strategy, + $this->variance, + $this->name, + $newBound, + ); + } + + return $this; + } + + protected function shouldGeneralizeInferredType(): bool + { + return false; + } + +} diff --git a/src/Type/Generic/TemplateConstantArrayType.php b/src/Type/Generic/TemplateConstantArrayType.php new file mode 100644 index 0000000000..f4496e0b79 --- /dev/null +++ b/src/Type/Generic/TemplateConstantArrayType.php @@ -0,0 +1,54 @@ + */ + use TemplateTypeTrait; + use UndecidedComparisonCompoundTypeTrait; + + public function __construct( + TemplateTypeScope $scope, + TemplateTypeStrategy $templateTypeStrategy, + TemplateTypeVariance $templateTypeVariance, + string $name, + ConstantArrayType $bound, + ) + { + parent::__construct($bound->getKeyTypes(), $bound->getValueTypes(), $bound->getNextAutoIndex(), $bound->getOptionalKeys()); + $this->scope = $scope; + $this->strategy = $templateTypeStrategy; + $this->variance = $templateTypeVariance; + $this->name = $name; + $this->bound = $bound; + } + + public function traverse(callable $cb): Type + { + $newBound = $cb($this->getBound()); + if ($this->getBound() !== $newBound && $newBound instanceof ConstantArrayType) { + return new self( + $this->scope, + $this->strategy, + $this->variance, + $this->name, + $newBound, + ); + } + + return $this; + } + + protected function shouldGeneralizeInferredType(): bool + { + return false; + } + +} diff --git a/src/Type/Generic/TemplateFloatType.php b/src/Type/Generic/TemplateFloatType.php new file mode 100644 index 0000000000..ca69695ae4 --- /dev/null +++ b/src/Type/Generic/TemplateFloatType.php @@ -0,0 +1,54 @@ + */ + use TemplateTypeTrait; + use UndecidedComparisonCompoundTypeTrait; + + public function __construct( + TemplateTypeScope $scope, + TemplateTypeStrategy $templateTypeStrategy, + TemplateTypeVariance $templateTypeVariance, + string $name, + FloatType $bound, + ) + { + parent::__construct(); + $this->scope = $scope; + $this->strategy = $templateTypeStrategy; + $this->variance = $templateTypeVariance; + $this->name = $name; + $this->bound = $bound; + } + + public function traverse(callable $cb): Type + { + $newBound = $cb($this->getBound()); + if ($this->getBound() !== $newBound && $newBound instanceof FloatType) { + return new self( + $this->scope, + $this->strategy, + $this->variance, + $this->name, + $newBound, + ); + } + + return $this; + } + + protected function shouldGeneralizeInferredType(): bool + { + return false; + } + +} diff --git a/src/Type/Generic/TemplateGenericObjectType.php b/src/Type/Generic/TemplateGenericObjectType.php index 2f5cd5616c..fc1b1d21af 100644 --- a/src/Type/Generic/TemplateGenericObjectType.php +++ b/src/Type/Generic/TemplateGenericObjectType.php @@ -18,7 +18,7 @@ public function __construct( TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, - GenericObjectType $bound + GenericObjectType $bound, ) { parent::__construct($bound->getClassName(), $bound->getTypes()); @@ -39,7 +39,7 @@ public function traverse(callable $cb): Type $this->strategy, $this->variance, $this->name, - $newBound + $newBound, ); } @@ -53,7 +53,7 @@ protected function recreate(string $className, array $types, ?Type $subtractedTy $this->strategy, $this->variance, $this->name, - $this->getBound() + $this->getBound(), ); } diff --git a/src/Type/Generic/TemplateIntegerType.php b/src/Type/Generic/TemplateIntegerType.php index 552d407a89..2b636d3fe5 100644 --- a/src/Type/Generic/TemplateIntegerType.php +++ b/src/Type/Generic/TemplateIntegerType.php @@ -19,7 +19,7 @@ public function __construct( TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, - IntegerType $bound + IntegerType $bound, ) { parent::__construct(); @@ -39,7 +39,7 @@ public function traverse(callable $cb): Type $this->strategy, $this->variance, $this->name, - $newBound + $newBound, ); } diff --git a/src/Type/Generic/TemplateMixedType.php b/src/Type/Generic/TemplateMixedType.php index 2c61b0885c..fd4a74701b 100644 --- a/src/Type/Generic/TemplateMixedType.php +++ b/src/Type/Generic/TemplateMixedType.php @@ -4,6 +4,7 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; +use PHPStan\Type\StrictMixedType; use PHPStan\Type\Type; /** @api */ @@ -18,7 +19,7 @@ public function __construct( TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, - MixedType $bound + MixedType $bound, ) { parent::__construct(true); @@ -53,11 +54,22 @@ public function traverse(callable $cb): Type $this->strategy, $this->variance, $this->name, - $newBound + $newBound, ); } return $this; } + public function toStrictMixedType(): TemplateStrictMixedType + { + return new TemplateStrictMixedType( + $this->scope, + $this->strategy, + $this->variance, + $this->name, + new StrictMixedType(), + ); + } + } diff --git a/src/Type/Generic/TemplateObjectType.php b/src/Type/Generic/TemplateObjectType.php index 8617a42858..906bf0fe31 100644 --- a/src/Type/Generic/TemplateObjectType.php +++ b/src/Type/Generic/TemplateObjectType.php @@ -19,7 +19,7 @@ public function __construct( TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, - ObjectType $bound + ObjectType $bound, ) { parent::__construct($bound->getClassName()); @@ -40,7 +40,7 @@ public function traverse(callable $cb): Type $this->strategy, $this->variance, $this->name, - $newBound + $newBound, ); } diff --git a/src/Type/Generic/TemplateObjectWithoutClassType.php b/src/Type/Generic/TemplateObjectWithoutClassType.php index 144a564e7c..4ed0e8ab7b 100644 --- a/src/Type/Generic/TemplateObjectWithoutClassType.php +++ b/src/Type/Generic/TemplateObjectWithoutClassType.php @@ -19,7 +19,7 @@ public function __construct( TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, - ObjectWithoutClassType $bound + ObjectWithoutClassType $bound, ) { parent::__construct(); @@ -40,7 +40,7 @@ public function traverse(callable $cb): Type $this->strategy, $this->variance, $this->name, - $newBound + $newBound, ); } diff --git a/src/Type/Generic/TemplateStrictMixedType.php b/src/Type/Generic/TemplateStrictMixedType.php new file mode 100644 index 0000000000..c1d4e8a4ee --- /dev/null +++ b/src/Type/Generic/TemplateStrictMixedType.php @@ -0,0 +1,58 @@ + */ + use TemplateTypeTrait; + + public function __construct( + TemplateTypeScope $scope, + TemplateTypeStrategy $templateTypeStrategy, + TemplateTypeVariance $templateTypeVariance, + string $name, + StrictMixedType $bound, + ) + { + $this->scope = $scope; + $this->strategy = $templateTypeStrategy; + $this->variance = $templateTypeVariance; + $this->name = $name; + $this->bound = $bound; + } + + public function isSuperTypeOfMixed(MixedType $type): TrinaryLogic + { + return $this->isSuperTypeOf($type); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic + { + return $this->isSubTypeOf($acceptingType); + } + + public function traverse(callable $cb): Type + { + $newBound = $cb($this->getBound()); + if ($this->getBound() !== $newBound && $newBound instanceof StrictMixedType) { + return new self( + $this->scope, + $this->strategy, + $this->variance, + $this->name, + $newBound, + ); + } + + return $this; + } + +} diff --git a/src/Type/Generic/TemplateStringType.php b/src/Type/Generic/TemplateStringType.php index 37a0e9b4e7..20130c8690 100644 --- a/src/Type/Generic/TemplateStringType.php +++ b/src/Type/Generic/TemplateStringType.php @@ -19,7 +19,7 @@ public function __construct( TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, - StringType $bound + StringType $bound, ) { parent::__construct(); @@ -39,7 +39,7 @@ public function traverse(callable $cb): Type $this->strategy, $this->variance, $this->name, - $newBound + $newBound, ); } diff --git a/src/Type/Generic/TemplateTypeArgumentStrategy.php b/src/Type/Generic/TemplateTypeArgumentStrategy.php index 07d1cc35e5..290dcbd336 100644 --- a/src/Type/Generic/TemplateTypeArgumentStrategy.php +++ b/src/Type/Generic/TemplateTypeArgumentStrategy.php @@ -3,8 +3,8 @@ namespace PHPStan\Type\Generic; use PHPStan\TrinaryLogic; +use PHPStan\Type\CompoundType; use PHPStan\Type\IntersectionType; -use PHPStan\Type\MixedType; use PHPStan\Type\Type; /** @@ -25,8 +25,14 @@ public function accepts(TemplateType $left, Type $right, bool $strictTypes): Tri return TrinaryLogic::createNo(); } - return TrinaryLogic::createFromBoolean($left->equals($right)) - ->or(TrinaryLogic::createFromBoolean($right->equals(new MixedType()))); + if ($right instanceof CompoundType) { + $accepts = $right->isAcceptedBy($left, $strictTypes); + } else { + $accepts = $left->getBound()->accepts($right, $strictTypes) + ->and(TrinaryLogic::createMaybe()); + } + + return $accepts; } public function isArgument(): bool diff --git a/src/Type/Generic/TemplateTypeFactory.php b/src/Type/Generic/TemplateTypeFactory.php index e6172790f8..6bad394abf 100644 --- a/src/Type/Generic/TemplateTypeFactory.php +++ b/src/Type/Generic/TemplateTypeFactory.php @@ -3,7 +3,11 @@ namespace PHPStan\Type\Generic; use PHPStan\PhpDoc\Tag\TemplateTag; +use PHPStan\Type\ArrayType; use PHPStan\Type\BenevolentUnionType; +use PHPStan\Type\BooleanType; +use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; @@ -11,6 +15,7 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; +use function get_class; final class TemplateTypeFactory { @@ -36,6 +41,14 @@ public static function create(TemplateTypeScope $scope, string $name, ?Type $bou return new TemplateObjectWithoutClassType($scope, $strategy, $variance, $name, $bound); } + if ($bound instanceof ArrayType && ($boundClass === ArrayType::class || $bound instanceof TemplateType)) { + return new TemplateArrayType($scope, $strategy, $variance, $name, $bound); + } + + if ($bound instanceof ConstantArrayType && ($boundClass === ConstantArrayType::class || $bound instanceof TemplateType)) { + return new TemplateConstantArrayType($scope, $strategy, $variance, $name, $bound); + } + if ($bound instanceof StringType && ($boundClass === StringType::class || $bound instanceof TemplateType)) { return new TemplateStringType($scope, $strategy, $variance, $name, $bound); } @@ -44,6 +57,14 @@ public static function create(TemplateTypeScope $scope, string $name, ?Type $bou return new TemplateIntegerType($scope, $strategy, $variance, $name, $bound); } + if ($bound instanceof FloatType && ($boundClass === FloatType::class || $bound instanceof TemplateType)) { + return new TemplateFloatType($scope, $strategy, $variance, $name, $bound); + } + + if ($bound instanceof BooleanType && ($boundClass === BooleanType::class || $bound instanceof TemplateType)) { + return new TemplateBooleanType($scope, $strategy, $variance, $name, $bound); + } + if ($bound instanceof MixedType && ($boundClass === MixedType::class || $bound instanceof TemplateType)) { return new TemplateMixedType($scope, $strategy, $variance, $name, $bound); } diff --git a/src/Type/Generic/TemplateTypeHelper.php b/src/Type/Generic/TemplateTypeHelper.php index 9c8165bcdc..f8e72514cf 100644 --- a/src/Type/Generic/TemplateTypeHelper.php +++ b/src/Type/Generic/TemplateTypeHelper.php @@ -2,8 +2,6 @@ namespace PHPStan\Type\Generic; -use PHPStan\Type\Constant\ConstantArrayType; -use PHPStan\Type\ConstantType; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; @@ -14,16 +12,16 @@ class TemplateTypeHelper /** * Replaces template types with standin types */ - public static function resolveTemplateTypes(Type $type, TemplateTypeMap $standins): Type + public static function resolveTemplateTypes(Type $type, TemplateTypeMap $standins, bool $keepErrorTypes = false): Type { - return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($standins): Type { + return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($standins, $keepErrorTypes): Type { if ($type instanceof TemplateType && !$type->isArgument()) { $newType = $standins->getType($type->getName()); if ($newType === null) { return $traverse($type); } - if ($newType instanceof ErrorType) { + if ($newType instanceof ErrorType && !$keepErrorTypes) { return $traverse($type->getBound()); } @@ -62,15 +60,4 @@ public static function toArgument(Type $type): Type }); } - public static function generalizeType(Type $type): Type - { - return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { - if ($type instanceof ConstantType && !$type instanceof ConstantArrayType) { - return $type->generalize(); - } - - return $traverse($type); - }); - } - } diff --git a/src/Type/Generic/TemplateTypeMap.php b/src/Type/Generic/TemplateTypeMap.php index 670b6a5f16..f807e3d65e 100644 --- a/src/Type/Generic/TemplateTypeMap.php +++ b/src/Type/Generic/TemplateTypeMap.php @@ -2,11 +2,12 @@ namespace PHPStan\Type\Generic; -use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function array_key_exists; +use function count; /** @api */ class TemplateTypeMap @@ -14,21 +15,15 @@ class TemplateTypeMap private static ?TemplateTypeMap $empty = null; - /** @var array */ - private array $types; - - /** @var array */ - private array $lowerBoundTypes; + private ?TemplateTypeMap $resolvedToBounds = null; /** * @api - * @param array $types - * @param array $lowerBoundTypes + * @param array $types + * @param array $lowerBoundTypes */ - public function __construct(array $types, array $lowerBoundTypes = []) + public function __construct(private array $types, private array $lowerBoundTypes = []) { - $this->types = $types; - $this->lowerBoundTypes = $lowerBoundTypes; } public function convertToLowerBoundTypes(): self @@ -65,15 +60,15 @@ public static function createEmpty(): self public function isEmpty(): bool { - return count($this->types) === 0; + return $this->count() === 0; } public function count(): int { - return count($this->types); + return count($this->types + $this->lowerBoundTypes); } - /** @return array */ + /** @return array */ public function getTypes(): array { $types = $this->types; @@ -210,14 +205,10 @@ public function map(callable $cb): self public function resolveToBounds(): self { - return $this->map(static function (string $name, Type $type): Type { - $type = TemplateTypeHelper::resolveToBounds($type); - if ($type instanceof MixedType && $type->isExplicitMixed()) { - return new MixedType(false); - } - - return $type; - }); + if ($this->resolvedToBounds !== null) { + return $this->resolvedToBounds; + } + return $this->resolvedToBounds = $this->map(static fn (string $name, Type $type): Type => TemplateTypeHelper::resolveToBounds($type)); } /** @@ -227,7 +218,7 @@ public static function __set_state(array $properties): self { return new self( $properties['types'], - $properties['lowerBoundTypes'] ?? [] + $properties['lowerBoundTypes'] ?? [], ); } diff --git a/src/Type/Generic/TemplateTypeParameterStrategy.php b/src/Type/Generic/TemplateTypeParameterStrategy.php index 89e69d6051..5d9a8256d6 100644 --- a/src/Type/Generic/TemplateTypeParameterStrategy.php +++ b/src/Type/Generic/TemplateTypeParameterStrategy.php @@ -4,7 +4,6 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\CompoundType; -use PHPStan\Type\CompoundTypeHelper; use PHPStan\Type\Type; /** @@ -16,7 +15,7 @@ class TemplateTypeParameterStrategy implements TemplateTypeStrategy public function accepts(TemplateType $left, Type $right, bool $strictTypes): TrinaryLogic { if ($right instanceof CompoundType) { - return CompoundTypeHelper::accepts($right, $left, $strictTypes); + return $right->isAcceptedBy($left, $strictTypes); } return $left->getBound()->accepts($right, $strictTypes); diff --git a/src/Type/Generic/TemplateTypeReference.php b/src/Type/Generic/TemplateTypeReference.php index a88e5c3ce1..260abd3d93 100644 --- a/src/Type/Generic/TemplateTypeReference.php +++ b/src/Type/Generic/TemplateTypeReference.php @@ -5,14 +5,8 @@ class TemplateTypeReference { - private TemplateType $type; - - private TemplateTypeVariance $positionVariance; - - public function __construct(TemplateType $type, TemplateTypeVariance $positionVariance) + public function __construct(private TemplateType $type, private TemplateTypeVariance $positionVariance) { - $this->type = $type; - $this->positionVariance = $positionVariance; } public function getType(): TemplateType diff --git a/src/Type/Generic/TemplateTypeScope.php b/src/Type/Generic/TemplateTypeScope.php index 8d33fcc943..f9a7625720 100644 --- a/src/Type/Generic/TemplateTypeScope.php +++ b/src/Type/Generic/TemplateTypeScope.php @@ -2,13 +2,11 @@ namespace PHPStan\Type\Generic; +use function sprintf; + class TemplateTypeScope { - private ?string $className; - - private ?string $functionName; - public static function createWithFunction(string $functionName): self { return new self(null, $functionName); @@ -24,10 +22,8 @@ public static function createWithClass(string $className): self return new self($className, null); } - private function __construct(?string $className, ?string $functionName) + private function __construct(private ?string $className, private ?string $functionName) { - $this->className = $className; - $this->functionName = $functionName; } /** @api */ @@ -70,7 +66,7 @@ public static function __set_state(array $properties): self { return new self( $properties['className'], - $properties['functionName'] + $properties['functionName'], ); } diff --git a/src/Type/Generic/TemplateTypeTrait.php b/src/Type/Generic/TemplateTypeTrait.php index 498c1de77a..2e9d3a9f5a 100644 --- a/src/Type/Generic/TemplateTypeTrait.php +++ b/src/Type/Generic/TemplateTypeTrait.php @@ -3,12 +3,15 @@ namespace PHPStan\Type\Generic; use PHPStan\TrinaryLogic; -use PHPStan\Type\CompoundType; +use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; +use PHPStan\Type\SubtractableType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function sprintf; /** * @template TBound of Type @@ -46,7 +49,8 @@ public function getBound(): Type public function describe(VerbosityLevel $level): string { $basicDescription = function () use ($level): string { - if ($this->bound instanceof MixedType) { // @phpstan-ignore-line + // @phpstan-ignore-next-line + if ($this->bound instanceof MixedType && $this->bound->getSubtractedType() === null && !$this->bound instanceof TemplateMixedType) { $boundDescription = ''; } else { // @phpstan-ignore-line $boundDescription = sprintf(' of %s', $this->bound->describe($level)); @@ -54,16 +58,14 @@ public function describe(VerbosityLevel $level): string return sprintf( '%s%s', $this->name, - $boundDescription + $boundDescription, ); }; return $level->handle( $basicDescription, $basicDescription, - function () use ($basicDescription): string { - return sprintf('%s (%s, %s)', $basicDescription(), $this->scope->describe(), $this->isArgument() ? 'argument' : 'parameter'); - } + fn (): string => sprintf('%s (%s, %s)', $basicDescription(), $this->scope->describe(), $this->isArgument() ? 'argument' : 'parameter'), ); } @@ -79,7 +81,7 @@ public function toArgument(): TemplateType new TemplateTypeArgumentStrategy(), $this->variance, $this->name, - TemplateTypeHelper::toArgument($this->getBound()) + TemplateTypeHelper::toArgument($this->getBound()), ); } @@ -88,19 +90,70 @@ public function isValidVariance(Type $a, Type $b): TrinaryLogic return $this->variance->isValidVariance($a, $b); } - public function subtract(Type $type): Type + public function subtract(Type $typeToRemove): Type { - return $this; + $removedBound = TypeCombinator::remove($this->getBound(), $typeToRemove); + $type = TemplateTypeFactory::create( + $this->getScope(), + $this->getName(), + $removedBound, + $this->getVariance(), + ); + if ($this->isArgument()) { + return TemplateTypeHelper::toArgument($type); + } + + return $type; } public function getTypeWithoutSubtractedType(): Type { - return $this; + $bound = $this->getBound(); + if (!$bound instanceof SubtractableType) { // @phpstan-ignore-line + return $this; + } + + $type = TemplateTypeFactory::create( + $this->getScope(), + $this->getName(), + $bound->getTypeWithoutSubtractedType(), + $this->getVariance(), + ); + if ($this->isArgument()) { + return TemplateTypeHelper::toArgument($type); + } + + return $type; } public function changeSubtractedType(?Type $subtractedType): Type { - return $this; + $bound = $this->getBound(); + if (!$bound instanceof SubtractableType) { // @phpstan-ignore-line + return $this; + } + + $type = TemplateTypeFactory::create( + $this->getScope(), + $this->getName(), + $bound->changeSubtractedType($subtractedType), + $this->getVariance(), + ); + if ($this->isArgument()) { + return TemplateTypeHelper::toArgument($type); + } + + return $type; + } + + public function getSubtractedType(): ?Type + { + $bound = $this->getBound(); + if (!$bound instanceof SubtractableType) { // @phpstan-ignore-line + return null; + } + + return $bound->getSubtractedType(); } public function equals(Type $type): bool @@ -113,7 +166,27 @@ public function equals(Type $type): bool public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic { - return $this->isSubTypeOf($acceptingType); + /** @var Type $bound */ + $bound = $this->getBound(); + if ( + !$acceptingType instanceof $bound + && !$this instanceof $acceptingType + && !$acceptingType instanceof TemplateType + && ($acceptingType instanceof UnionType || $acceptingType instanceof IntersectionType) + ) { + return $acceptingType->accepts($this, $strictTypes); + } + + if (!$acceptingType instanceof TemplateType) { + return $acceptingType->accepts($this->getBound(), $strictTypes); + } + + if ($this->getScope()->equals($acceptingType->getScope()) && $this->getName() === $acceptingType->getName()) { + return $acceptingType->getBound()->accepts($this->getBound(), $strictTypes); + } + + return $acceptingType->getBound()->accepts($this->getBound(), $strictTypes) + ->and(TrinaryLogic::createMaybe()); } public function accepts(Type $type, bool $strictTypes): TrinaryLogic @@ -123,7 +196,7 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic public function isSuperTypeOf(Type $type): TrinaryLogic { - if ($type instanceof CompoundType) { + if ($type instanceof TemplateType || $type instanceof IntersectionType) { return $type->isSubTypeOf($this); } @@ -148,21 +221,17 @@ public function isSubTypeOf(Type $type): TrinaryLogic return $type->isSuperTypeOf($this->getBound()); } - if ($this->equals($type)) { - return TrinaryLogic::createYes(); + if ($this->getScope()->equals($type->getScope()) && $this->getName() === $type->getName()) { + return $type->getBound()->isSuperTypeOf($this->getBound()); } - if ($type->getBound()->isSuperTypeOf($this->getBound())->no() && - $this->getBound()->isSuperTypeOf($type->getBound())->no()) { - return TrinaryLogic::createNo(); - } - - return TrinaryLogic::createMaybe(); + return $type->getBound()->isSuperTypeOf($this->getBound()) + ->and(TrinaryLogic::createMaybe()); } public function inferTemplateTypes(Type $receivedType): TemplateTypeMap { - if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) { + if (!$receivedType instanceof TemplateType && ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType)) { return $receivedType->inferTemplateTypesOn($this); } @@ -179,7 +248,7 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap $resolvedBound = TemplateTypeHelper::resolveTemplateTypes($this->getBound(), $map); if ($resolvedBound->isSuperTypeOf($receivedType)->yes()) { return (new TemplateTypeMap([ - $this->name => $this->shouldGeneralizeInferredType() ? TemplateTypeHelper::generalizeType($receivedType) : $receivedType, + $this->name => $this->shouldGeneralizeInferredType() ? $receivedType->generalize(GeneralizePrecision::templateArgument()) : $receivedType, ]))->union($map); } @@ -201,9 +270,17 @@ protected function shouldGeneralizeInferredType(): bool return true; } + public function tryRemove(Type $typeToRemove): ?Type + { + if ($this->getBound()->isSuperTypeOf($typeToRemove)->yes()) { + return $this->subtract($typeToRemove); + } + + return null; + } + /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { @@ -212,7 +289,7 @@ public static function __set_state(array $properties): Type $properties['strategy'], $properties['variance'], $properties['name'], - $properties['bound'] + $properties['bound'], ); } diff --git a/src/Type/Generic/TemplateTypeVariance.php b/src/Type/Generic/TemplateTypeVariance.php index 3a0aa9217f..edd59b7c24 100644 --- a/src/Type/Generic/TemplateTypeVariance.php +++ b/src/Type/Generic/TemplateTypeVariance.php @@ -2,9 +2,11 @@ namespace PHPStan\Type\Generic; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\MixedType; +use PHPStan\Type\NeverType; use PHPStan\Type\Type; /** @api */ @@ -19,16 +21,13 @@ class TemplateTypeVariance /** @var self[] */ private static array $registry; - private int $value; - - private function __construct(int $value) + private function __construct(private int $value) { - $this->value = $value; } private static function create(int $value): self { - self::$registry[$value] = self::$registry[$value] ?? new self($value); + self::$registry[$value] ??= new self($value); return self::$registry[$value]; } @@ -99,6 +98,10 @@ public function compose(self $other): self public function isValidVariance(Type $a, Type $b): TrinaryLogic { + if ($b instanceof NeverType) { + return TrinaryLogic::createYes(); + } + if ($a instanceof MixedType && !$a instanceof TemplateType) { return TrinaryLogic::createYes(); } @@ -131,7 +134,7 @@ public function isValidVariance(Type $a, Type $b): TrinaryLogic return $b->isSuperTypeOf($a); } - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function equals(self $other): bool @@ -159,12 +162,11 @@ public function describe(): string return 'static'; } - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } /** * @param array{value: int} $properties - * @return self */ public static function __set_state(array $properties): self { diff --git a/src/Type/Generic/TemplateUnionType.php b/src/Type/Generic/TemplateUnionType.php index 2dea6a3c1e..521a3fb536 100644 --- a/src/Type/Generic/TemplateUnionType.php +++ b/src/Type/Generic/TemplateUnionType.php @@ -17,7 +17,7 @@ public function __construct( TemplateTypeStrategy $templateTypeStrategy, TemplateTypeVariance $templateTypeVariance, string $name, - UnionType $bound + UnionType $bound, ) { parent::__construct($bound->getTypes()); @@ -38,7 +38,7 @@ public function traverse(callable $cb): Type $this->strategy, $this->variance, $this->name, - $newBound + $newBound, ); } diff --git a/src/Type/GenericTypeVariableResolver.php b/src/Type/GenericTypeVariableResolver.php index 5d6abefe43..fc670ee662 100644 --- a/src/Type/GenericTypeVariableResolver.php +++ b/src/Type/GenericTypeVariableResolver.php @@ -2,6 +2,8 @@ namespace PHPStan\Type; +use PHPStan\Type\Generic\TemplateTypeHelper; + /** @api */ class GenericTypeVariableResolver { @@ -9,22 +11,37 @@ class GenericTypeVariableResolver public static function getType( TypeWithClassName $type, string $genericClassName, - string $typeVariableName + string $typeVariableName, ): ?Type { - $ancestor = $type->getAncestorWithClassName($genericClassName); - if ($ancestor === null) { + $classReflection = $type->getClassReflection(); + if ($classReflection === null) { return null; } - - $classReflection = $ancestor->getClassReflection(); - if ($classReflection === null) { + $ancestorClassReflection = $classReflection->getAncestorWithClassName($genericClassName); + if ($ancestorClassReflection === null) { return null; } - $templateTypeMap = $classReflection->getActiveTemplateTypeMap(); + $activeTemplateTypeMap = $ancestorClassReflection->getPossiblyIncompleteActiveTemplateTypeMap(); + + $type = $activeTemplateTypeMap->getType($typeVariableName); + if ($type instanceof ErrorType) { + $templateTypeMap = $ancestorClassReflection->getTemplateTypeMap(); + $templateType = $templateTypeMap->getType($typeVariableName); + if ($templateType === null) { + return $type; + } + + $bound = TemplateTypeHelper::resolveToBounds($templateType); + if ($bound instanceof MixedType && $bound->isExplicitMixed()) { + return new MixedType(false); + } + + return $bound; + } - return $templateTypeMap->getType($typeVariableName); + return $type; } } diff --git a/src/Type/IntegerRangeType.php b/src/Type/IntegerRangeType.php index 1703fd5a25..cc1e2035fd 100644 --- a/src/Type/IntegerRangeType.php +++ b/src/Type/IntegerRangeType.php @@ -5,24 +5,27 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use function assert; +use function ceil; +use function floor; +use function get_class; +use function is_int; +use function max; +use function min; +use function sprintf; +use const PHP_INT_MAX; +use const PHP_INT_MIN; /** @api */ class IntegerRangeType extends IntegerType implements CompoundType { - private ?int $min; - - private ?int $max; - - public function __construct(?int $min, ?int $max) + public function __construct(private ?int $min, private ?int $max) { // this constructor can be made private when PHP 7.2 is the minimum parent::__construct(); assert($min === null || $max === null || $min <= $max); assert($min !== null || $max !== null); - - $this->min = $min; - $this->max = $max; } public static function fromInterval(?int $min, ?int $max, int $shift = 0): Type @@ -54,7 +57,6 @@ protected static function isDisjoint(?int $minA, ?int $maxA, ?int $minB, ?int $m * Return the range of integers smaller than the given value * * @param int|float $value - * @return Type */ public static function createAllSmallerThan($value): Type { @@ -77,7 +79,6 @@ public static function createAllSmallerThan($value): Type * Return the range of integers smaller than or equal to the given value * * @param int|float $value - * @return Type */ public static function createAllSmallerThanOrEqualTo($value): Type { @@ -100,7 +101,6 @@ public static function createAllSmallerThanOrEqualTo($value): Type * Return the range of integers greater than the given value * * @param int|float $value - * @return Type */ public static function createAllGreaterThan($value): Type { @@ -123,7 +123,6 @@ public static function createAllGreaterThan($value): Type * Return the range of integers greater than or equal to the given value * * @param int|float $value - * @return Type */ public static function createAllGreaterThanOrEqualTo($value): Type { @@ -202,7 +201,7 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic } if ($type instanceof CompoundType) { - return CompoundTypeHelper::accepts($type, $this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } return TrinaryLogic::createNo(); @@ -269,7 +268,7 @@ public function equals(Type $type): bool } - public function generalize(): Type + public function generalize(GeneralizePrecision $precision): Type { return new parent(); } @@ -400,11 +399,6 @@ public function getGreaterOrEqualType(): Type return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes)); } - public function toNumber(): Type - { - return new parent(); - } - public function toBoolean(): BooleanType { $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($this); @@ -422,8 +416,6 @@ public function toBoolean(): BooleanType /** * Return the union with another type, but only if it can be expressed in a simpler way than using UnionType * - * @param Type $otherType - * @return Type|null */ public function tryUnion(Type $otherType): ?Type { @@ -442,7 +434,7 @@ public function tryUnion(Type $otherType): ?Type return self::fromInterval( $this->min !== null && $otherMin !== null ? min($this->min, $otherMin) : null, - $this->max !== null && $otherMax !== null ? max($this->max, $otherMax) : null + $this->max !== null && $otherMax !== null ? max($this->max, $otherMax) : null, ); } @@ -457,8 +449,6 @@ public function tryUnion(Type $otherType): ?Type * Return the intersection with another type, but only if it can be expressed in a simpler way than using * IntersectionType * - * @param Type $otherType - * @return Type|null */ public function tryIntersect(Type $otherType): ?Type { @@ -504,8 +494,6 @@ public function tryIntersect(Type $otherType): ?Type /** * Return the different with another type, or null if it cannot be represented. * - * @param Type $typeToRemove - * @return Type|null */ public function tryRemove(Type $typeToRemove): ?Type { @@ -513,8 +501,8 @@ public function tryRemove(Type $typeToRemove): ?Type return new NeverType(); } - if ($typeToRemove instanceof IntegerRangeType || $typeToRemove instanceof ConstantIntegerType) { - if ($typeToRemove instanceof IntegerRangeType) { + if ($typeToRemove instanceof self || $typeToRemove instanceof ConstantIntegerType) { + if ($typeToRemove instanceof self) { $removeMin = $typeToRemove->min; $removeMax = $typeToRemove->max; } else { @@ -552,7 +540,6 @@ public function tryRemove(Type $typeToRemove): ?Type /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { diff --git a/src/Type/IntegerType.php b/src/Type/IntegerType.php index b562754258..3ff1e48778 100644 --- a/src/Type/IntegerType.php +++ b/src/Type/IntegerType.php @@ -2,14 +2,15 @@ namespace PHPStan\Type; -use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Traits\NonCallableTypeTrait; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; +use PHPStan\Type\Traits\NonOffsetAccessibleTypeTrait; use PHPStan\Type\Traits\UndecidedBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; @@ -24,6 +25,8 @@ class IntegerType implements Type use UndecidedBooleanTypeTrait; use UndecidedComparisonTypeTrait; use NonGenericTypeTrait; + use NonOffsetAccessibleTypeTrait; + use NonGeneralizableTypeTrait; /** @api */ public function __construct() @@ -37,7 +40,6 @@ public function describe(VerbosityLevel $level): string /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { @@ -72,28 +74,29 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1 + 1, ); } - public function isOffsetAccessible(): TrinaryLogic + public function tryRemove(Type $typeToRemove): ?Type { - return TrinaryLogic::createNo(); - } - - public function hasOffsetValueType(Type $offsetType): TrinaryLogic - { - return TrinaryLogic::createNo(); - } - - public function getOffsetValueType(Type $offsetType): Type - { - return new ErrorType(); - } - - public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type - { - return new ErrorType(); + if ($typeToRemove instanceof IntegerRangeType || $typeToRemove instanceof ConstantIntegerType) { + if ($typeToRemove instanceof IntegerRangeType) { + $removeValueMin = $typeToRemove->getMin(); + $removeValueMax = $typeToRemove->getMax(); + } else { + $removeValueMin = $typeToRemove->getValue(); + $removeValueMax = $typeToRemove->getValue(); + } + $lowerPart = $removeValueMin !== null ? IntegerRangeType::fromInterval(null, $removeValueMin, -1) : null; + $upperPart = $removeValueMax !== null ? IntegerRangeType::fromInterval($removeValueMax, null, +1) : null; + if ($lowerPart !== null && $upperPart !== null) { + return new UnionType([$lowerPart, $upperPart]); + } + return $lowerPart ?? $upperPart ?? new NeverType(); + } + + return null; } } diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index e1e2d3ff4d..c79e878cca 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -5,24 +5,41 @@ use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\Reflection\Type\IntersectionTypeUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\IntersectionTypeUnresolvedPropertyPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Accessory\AccessoryType; use PHPStan\Type\Accessory\NonEmptyArrayType; +use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; +use function array_map; +use function count; +use function implode; +use function in_array; +use function sprintf; +use function strlen; +use function substr; /** @api */ class IntersectionType implements CompoundType { - /** @var \PHPStan\Type\Type[] */ + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; + + /** @var Type[] */ private array $types; /** @@ -31,6 +48,14 @@ class IntersectionType implements CompoundType */ public function __construct(array $types) { + if (count($types) < 2) { + throw new ShouldNotHappenException(sprintf( + 'Cannot create %s with: %s', + self::class, + implode(', ', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::value()), $types)), + )); + } + $this->types = UnionTypeHelper::sortTypes($types); } @@ -77,7 +102,7 @@ public function isSuperTypeOf(Type $otherType): TrinaryLogic public function isSubTypeOf(Type $otherType): TrinaryLogic { - if ($otherType instanceof self || $otherType instanceof UnionType) { + if (($otherType instanceof self || $otherType instanceof UnionType) && !$otherType instanceof TemplateType) { return $otherType->isSuperTypeOf($this); } @@ -131,45 +156,87 @@ function () use ($level): string { if ($type instanceof AccessoryType) { continue; } - $typeNames[] = TypeUtils::generalizeType($type)->describe($level); + $typeNames[] = $type->generalize(GeneralizePrecision::lessSpecific())->describe($level); } return implode('&', $typeNames); }, - function () use ($level): string { - $typeNames = []; - foreach ($this->types as $type) { - if ($type instanceof AccessoryType && !$type instanceof AccessoryNumericStringType && !$type instanceof NonEmptyArrayType) { + fn (): string => $this->describeItself($level, true), + fn (): string => $this->describeItself($level, false), + ); + } + + private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes): string + { + $typesToDescribe = []; + $skipTypeNames = []; + foreach ($this->types as $type) { + if ($type instanceof AccessoryNonEmptyStringType || $type instanceof AccessoryLiteralStringType || $type instanceof AccessoryNumericStringType) { + $typesToDescribe[] = $type; + $skipTypeNames[] = 'string'; + continue; + } + if ($type instanceof NonEmptyArrayType) { + $typesToDescribe[] = $type; + $skipTypeNames[] = 'array'; + continue; + } + + if ($skipAccessoryTypes) { + continue; + } + + if (!$type instanceof AccessoryType) { + continue; + } + + $typesToDescribe[] = $type; + } + + $describedTypes = []; + foreach ($this->types as $type) { + if ($type instanceof AccessoryType) { + continue; + } + $typeDescription = $type->describe($level); + if ( + substr($typeDescription, 0, strlen('array<')) === 'array<' + && in_array('array', $skipTypeNames, true) + ) { + foreach ($typesToDescribe as $j => $typeToDescribe) { + if (!$typeToDescribe instanceof NonEmptyArrayType) { continue; } - $typeNames[] = $type->describe($level); - } - return implode('&', $typeNames); - }, - function () use ($level): string { - $typeNames = []; - foreach ($this->types as $type) { - $typeNames[] = $type->describe($level); + unset($typesToDescribe[$j]); } - return implode('&', $typeNames); + $describedTypes[] = 'non-empty-array<' . substr($typeDescription, strlen('array<')); + continue; } - ); + + if (in_array($typeDescription, $skipTypeNames, true)) { + continue; + } + + $describedTypes[] = $type->describe($level); + } + + foreach ($typesToDescribe as $typeToDescribe) { + $describedTypes[] = $typeToDescribe->describe($level); + } + + return implode('&', $describedTypes); } public function canAccessProperties(): TrinaryLogic { - return $this->intersectResults(static function (Type $type): TrinaryLogic { - return $type->canAccessProperties(); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canAccessProperties()); } public function hasProperty(string $propertyName): TrinaryLogic { - return $this->intersectResults(static function (Type $type) use ($propertyName): TrinaryLogic { - return $type->hasProperty($propertyName); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasProperty($propertyName)); } public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection @@ -190,7 +257,7 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember $propertiesCount = count($propertyPrototypes); if ($propertiesCount === 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ($propertiesCount === 1) { @@ -202,16 +269,12 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember public function canCallMethods(): TrinaryLogic { - return $this->intersectResults(static function (Type $type): TrinaryLogic { - return $type->canCallMethods(); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canCallMethods()); } public function hasMethod(string $methodName): TrinaryLogic { - return $this->intersectResults(static function (Type $type) use ($methodName): TrinaryLogic { - return $type->hasMethod($methodName); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasMethod($methodName)); } public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection @@ -232,7 +295,7 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce $methodsCount = count($methodPrototypes); if ($methodsCount === 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ($methodsCount === 1) { @@ -244,16 +307,12 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce public function canAccessConstants(): TrinaryLogic { - return $this->intersectResults(static function (Type $type): TrinaryLogic { - return $type->canAccessConstants(); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canAccessConstants()); } public function hasConstant(string $constantName): TrinaryLogic { - return $this->intersectResults(static function (Type $type) use ($constantName): TrinaryLogic { - return $type->hasConstant($constantName); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName)); } public function getConstant(string $constantName): ConstantReflection @@ -264,94 +323,91 @@ public function getConstant(string $constantName): ConstantReflection } } - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function isIterable(): TrinaryLogic { - return $this->intersectResults(static function (Type $type): TrinaryLogic { - return $type->isIterable(); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isIterable()); } public function isIterableAtLeastOnce(): TrinaryLogic { - return $this->intersectResults(static function (Type $type): TrinaryLogic { - return $type->isIterableAtLeastOnce(); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isIterableAtLeastOnce()); } public function getIterableKeyType(): Type { - return $this->intersectTypes(static function (Type $type): Type { - return $type->getIterableKeyType(); - }); + return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableKeyType()); } public function getIterableValueType(): Type { - return $this->intersectTypes(static function (Type $type): Type { - return $type->getIterableValueType(); - }); + return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType()); } public function isArray(): TrinaryLogic { - return $this->intersectResults(static function (Type $type): TrinaryLogic { - return $type->isArray(); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isArray()); + } + + public function isString(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isString()); } public function isNumericString(): TrinaryLogic { - return $this->intersectResults(static function (Type $type): TrinaryLogic { - return $type->isNumericString(); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNumericString()); + } + + public function isNonEmptyString(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNonEmptyString()); + } + + public function isLiteralString(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isLiteralString()); } public function isOffsetAccessible(): TrinaryLogic { - return $this->intersectResults(static function (Type $type): TrinaryLogic { - return $type->isOffsetAccessible(); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessible()); } public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - return $this->intersectResults(static function (Type $type) use ($offsetType): TrinaryLogic { - return $type->hasOffsetValueType($offsetType); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType)); } public function getOffsetValueType(Type $offsetType): Type { - return $this->intersectTypes(static function (Type $type) use ($offsetType): Type { - return $type->getOffsetValueType($offsetType); - }); + return $this->intersectTypes(static fn (Type $type): Type => $type->getOffsetValueType($offsetType)); } public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { - return $this->intersectTypes(static function (Type $type) use ($offsetType, $valueType, $unionValues): Type { - return $type->setOffsetValueType($offsetType, $valueType, $unionValues); - }); + return $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues)); + } + + public function unsetOffset(Type $offsetType): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->unsetOffset($offsetType)); } public function isCallable(): TrinaryLogic { - return $this->intersectResults(static function (Type $type): TrinaryLogic { - return $type->isCallable(); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isCallable()); } /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] + * @return ParametersAcceptor[] */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { if ($this->isCallable()->no()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return [new TrivialParametersAcceptor()]; @@ -359,72 +415,52 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) public function isCloneable(): TrinaryLogic { - return $this->intersectResults(static function (Type $type): TrinaryLogic { - return $type->isCloneable(); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isCloneable()); } public function isSmallerThan(Type $otherType): TrinaryLogic { - return $this->intersectResults(static function (Type $type) use ($otherType): TrinaryLogic { - return $type->isSmallerThan($otherType); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThan($otherType)); } public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic { - return $this->intersectResults(static function (Type $type) use ($otherType): TrinaryLogic { - return $type->isSmallerThanOrEqual($otherType); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType)); } public function isGreaterThan(Type $otherType): TrinaryLogic { - return $this->intersectResults(static function (Type $type) use ($otherType): TrinaryLogic { - return $otherType->isSmallerThan($type); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type)); } public function isGreaterThanOrEqual(Type $otherType): TrinaryLogic { - return $this->intersectResults(static function (Type $type) use ($otherType): TrinaryLogic { - return $otherType->isSmallerThanOrEqual($type); - }); + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type)); } public function getSmallerType(): Type { - return $this->intersectTypes(static function (Type $type): Type { - return $type->getSmallerType(); - }); + return $this->intersectTypes(static fn (Type $type): Type => $type->getSmallerType()); } public function getSmallerOrEqualType(): Type { - return $this->intersectTypes(static function (Type $type): Type { - return $type->getSmallerOrEqualType(); - }); + return $this->intersectTypes(static fn (Type $type): Type => $type->getSmallerOrEqualType()); } public function getGreaterType(): Type { - return $this->intersectTypes(static function (Type $type): Type { - return $type->getGreaterType(); - }); + return $this->intersectTypes(static fn (Type $type): Type => $type->getGreaterType()); } public function getGreaterOrEqualType(): Type { - return $this->intersectTypes(static function (Type $type): Type { - return $type->getGreaterOrEqualType(); - }); + return $this->intersectTypes(static fn (Type $type): Type => $type->getGreaterOrEqualType()); } public function toBoolean(): BooleanType { - $type = $this->intersectTypes(static function (Type $type): BooleanType { - return $type->toBoolean(); - }); + $type = $this->intersectTypes(static fn (Type $type): BooleanType => $type->toBoolean()); if (!$type instanceof BooleanType) { return new BooleanType(); @@ -435,45 +471,35 @@ public function toBoolean(): BooleanType public function toNumber(): Type { - $type = $this->intersectTypes(static function (Type $type): Type { - return $type->toNumber(); - }); + $type = $this->intersectTypes(static fn (Type $type): Type => $type->toNumber()); return $type; } public function toString(): Type { - $type = $this->intersectTypes(static function (Type $type): Type { - return $type->toString(); - }); + $type = $this->intersectTypes(static fn (Type $type): Type => $type->toString()); return $type; } public function toInteger(): Type { - $type = $this->intersectTypes(static function (Type $type): Type { - return $type->toInteger(); - }); + $type = $this->intersectTypes(static fn (Type $type): Type => $type->toInteger()); return $type; } public function toFloat(): Type { - $type = $this->intersectTypes(static function (Type $type): Type { - return $type->toFloat(); - }); + $type = $this->intersectTypes(static fn (Type $type): Type => $type->toFloat()); return $type; } public function toArray(): Type { - $type = $this->intersectTypes(static function (Type $type): Type { - return $type->toArray(); - }); + $type = $this->intersectTypes(static fn (Type $type): Type => $type->toArray()); return $type; } @@ -533,9 +559,13 @@ public function traverse(callable $cb): Type return $this; } + public function tryRemove(Type $typeToRemove): ?Type + { + return $this->intersectTypes(static fn (Type $type): Type => TypeCombinator::remove($type, $typeToRemove)); + } + /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { @@ -544,7 +574,6 @@ public static function __set_state(array $properties): Type /** * @param callable(Type $type): TrinaryLogic $getResult - * @return TrinaryLogic */ private function intersectResults(callable $getResult): TrinaryLogic { @@ -554,7 +583,6 @@ private function intersectResults(callable $getResult): TrinaryLogic /** * @param callable(Type $type): Type $getType - * @return Type */ private function intersectTypes(callable $getType): Type { diff --git a/src/Type/IterableType.php b/src/Type/IterableType.php index 77a89de4a3..3b696ed514 100644 --- a/src/Type/IterableType.php +++ b/src/Type/IterableType.php @@ -4,6 +4,7 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateMixedType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeMap; @@ -11,8 +12,12 @@ use PHPStan\Type\Traits\MaybeCallableTypeTrait; use PHPStan\Type\Traits\MaybeObjectTypeTrait; use PHPStan\Type\Traits\MaybeOffsetAccessibleTypeTrait; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\UndecidedBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use Traversable; +use function array_merge; +use function sprintf; /** @api */ class IterableType implements CompoundType @@ -23,19 +28,14 @@ class IterableType implements CompoundType use MaybeOffsetAccessibleTypeTrait; use UndecidedBooleanTypeTrait; use UndecidedComparisonCompoundTypeTrait; - - private \PHPStan\Type\Type $keyType; - - private \PHPStan\Type\Type $itemType; + use NonGeneralizableTypeTrait; /** @api */ public function __construct( - Type $keyType, - Type $itemType + private Type $keyType, + private Type $itemType, ) { - $this->keyType = $keyType; - $this->itemType = $itemType; } public function getKeyType(): Type @@ -55,7 +55,7 @@ public function getReferencedClasses(): array { return array_merge( $this->keyType->getReferencedClasses(), - $this->getItemType()->getReferencedClasses() + $this->getItemType()->getReferencedClasses(), ); } @@ -70,7 +70,7 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic } if ($type instanceof CompoundType) { - return CompoundTypeHelper::accepts($type, $this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } return TrinaryLogic::createNo(); @@ -117,7 +117,7 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic return $otherType->isSuperTypeOf(new UnionType([ new ArrayType($this->keyType, $this->itemType), new IntersectionType([ - new ObjectType(\Traversable::class), + new ObjectType(Traversable::class), $this, ]), ])); @@ -136,7 +136,7 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic return $limit->and( $otherType->isIterable(), $otherType->getIterableValueType()->isSuperTypeOf($this->itemType), - $otherType->getIterableKeyType()->isSuperTypeOf($this->keyType) + $otherType->getIterableKeyType()->isSuperTypeOf($this->keyType), ); } @@ -230,11 +230,26 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNumericString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap { if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) { @@ -255,7 +270,7 @@ public function getReferencedTemplateTypes(TemplateTypeVariance $positionVarianc { return array_merge( $this->getIterableKeyType()->getReferencedTemplateTypes(TemplateTypeVariance::createCovariant()), - $this->getIterableValueType()->getReferencedTemplateTypes(TemplateTypeVariance::createCovariant()) + $this->getIterableValueType()->getReferencedTemplateTypes(TemplateTypeVariance::createCovariant()), ); } @@ -271,9 +286,26 @@ public function traverse(callable $cb): Type return $this; } + public function tryRemove(Type $typeToRemove): ?Type + { + $arrayType = new ArrayType(new MixedType(), new MixedType()); + if ($typeToRemove->isSuperTypeOf($arrayType)->yes()) { + return new GenericObjectType(Traversable::class, [ + $this->getIterableKeyType(), + $this->getIterableValueType(), + ]); + } + + $traversableType = new ObjectType(Traversable::class); + if ($typeToRemove->isSuperTypeOf($traversableType)->yes()) { + return new ArrayType($this->getIterableKeyType(), $this->getIterableValueType()); + } + + return null; + } + /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { diff --git a/src/Type/JustNullableTypeTrait.php b/src/Type/JustNullableTypeTrait.php index d56223fa0f..e1c10120b1 100644 --- a/src/Type/JustNullableTypeTrait.php +++ b/src/Type/JustNullableTypeTrait.php @@ -3,6 +3,7 @@ namespace PHPStan\Type; use PHPStan\TrinaryLogic; +use function get_class; trait JustNullableTypeTrait { @@ -22,7 +23,7 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic } if ($type instanceof CompoundType) { - return CompoundTypeHelper::accepts($type, $this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } return TrinaryLogic::createNo(); @@ -57,9 +58,24 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNumericString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + } diff --git a/src/Type/LazyTypeAliasResolverProvider.php b/src/Type/LazyTypeAliasResolverProvider.php new file mode 100644 index 0000000000..d270e691f2 --- /dev/null +++ b/src/Type/LazyTypeAliasResolverProvider.php @@ -0,0 +1,19 @@ +container->getByType(TypeAliasResolver::class); + } + +} diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 6a8fb51971..a813645869 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -8,6 +8,7 @@ use PHPStan\Reflection\Dummy\DummyMethodReflection; use PHPStan\Reflection\Dummy\DummyPropertyReflection; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\Reflection\Type\CallbackUnresolvedMethodPrototypeReflection; @@ -18,35 +19,31 @@ use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Generic\TemplateMixedType; use PHPStan\Type\Generic\TemplateType; -use PHPStan\Type\Traits\MaybeIterableTypeTrait; -use PHPStan\Type\Traits\MaybeOffsetAccessibleTypeTrait; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; +use function sprintf; /** @api */ class MixedType implements CompoundType, SubtractableType { - use MaybeIterableTypeTrait; - use MaybeOffsetAccessibleTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonCompoundTypeTrait; + use NonGeneralizableTypeTrait; - private bool $isExplicitMixed; - - private ?\PHPStan\Type\Type $subtractedType; + private ?Type $subtractedType; /** @api */ public function __construct( - bool $isExplicitMixed = false, - ?Type $subtractedType = null + private bool $isExplicitMixed = false, + ?Type $subtractedType = null, ) { if ($subtractedType instanceof NeverType) { $subtractedType = null; } - $this->isExplicitMixed = $isExplicitMixed; $this->subtractedType = $subtractedType; } @@ -118,7 +115,12 @@ public function isSuperTypeOf(Type $type): TrinaryLogic public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { - return new MixedType(); + return new self($this->isExplicitMixed); + } + + public function unsetOffset(Type $offsetType): Type + { + return $this; } public function isCallable(): TrinaryLogic @@ -134,8 +136,7 @@ public function isCallable(): TrinaryLogic } /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] + * @return ParametersAcceptor[] */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { @@ -210,9 +211,7 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember $property, $property->getDeclaringClass(), false, - static function (Type $type): Type { - return $type; - } + static fn (Type $type): Type => $type, ); } @@ -238,9 +237,7 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce $method, $method->getDeclaringClass(), false, - static function (Type $type): Type { - return $type; - } + static fn (Type $type): Type => $type, ); } @@ -267,12 +264,8 @@ public function isCloneable(): TrinaryLogic public function describe(VerbosityLevel $level): string { return $level->handle( - static function (): string { - return 'mixed'; - }, - static function (): string { - return 'mixed'; - }, + static fn (): string => 'mixed', + static fn (): string => 'mixed', function () use ($level): string { $description = 'mixed'; if ($this->subtractedType !== null) { @@ -294,7 +287,7 @@ function () use ($level): string { } return $description; - } + }, ); } @@ -332,7 +325,44 @@ public function toString(): Type public function toArray(): Type { - return new ArrayType(new MixedType(), new MixedType()); + $mixed = new self($this->isExplicitMixed); + + return new ArrayType($mixed, $mixed); + } + + public function isIterable(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isIterableAtLeastOnce(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getIterableKeyType(): Type + { + return new self($this->isExplicitMixed); + } + + public function getIterableValueType(): Type + { + return new self($this->isExplicitMixed); + } + + public function isOffsetAccessible(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function hasOffsetValueType(Type $offsetType): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getOffsetValueType(Type $offsetType): Type + { + return new self($this->isExplicitMixed); } public function isExplicitMixed(): bool @@ -377,20 +407,43 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isNumericString(): TrinaryLogic { return TrinaryLogic::createMaybe(); } + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function tryRemove(Type $typeToRemove): ?Type + { + if ($this->isSuperTypeOf($typeToRemove)->yes()) { + return $this->subtract($typeToRemove); + } + + return null; + } + /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { return new self( $properties['isExplicitMixed'], - $properties['subtractedType'] ?? null + $properties['subtractedType'] ?? null, ); } diff --git a/src/Type/NeverType.php b/src/Type/NeverType.php index 663a37940b..ec0c5a66dd 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -5,29 +5,32 @@ use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; -use PHPStan\Type\Traits\FalseyBooleanTypeTrait; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; +use PHPStan\Type\Traits\UndecidedBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; /** @api */ class NeverType implements CompoundType { - use FalseyBooleanTypeTrait; + use UndecidedBooleanTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonCompoundTypeTrait; - - private bool $isExplicit; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; /** @api */ - public function __construct(bool $isExplicit = false) + public function __construct(private bool $isExplicit = false) { - $this->isExplicit = $isExplicit; } public function isExplicit(): bool @@ -89,12 +92,12 @@ public function hasProperty(string $propertyName): TrinaryLogic public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function canCallMethods(): TrinaryLogic @@ -109,12 +112,12 @@ public function hasMethod(string $methodName): TrinaryLogic public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function canAccessConstants(): TrinaryLogic @@ -129,7 +132,7 @@ public function hasConstant(string $constantName): TrinaryLogic public function getConstant(string $constantName): ConstantReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function isIterable(): TrinaryLogic @@ -172,14 +175,18 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return new NeverType(); } + public function unsetOffset(Type $offsetType): Type + { + return new NeverType(); + } + public function isCallable(): TrinaryLogic { return TrinaryLogic::createYes(); } /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] + * @return ParametersAcceptor[] */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { @@ -226,14 +233,28 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNumericString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { diff --git a/src/Type/NonexistentParentClassType.php b/src/Type/NonexistentParentClassType.php index 1731f72b92..21f5d783eb 100644 --- a/src/Type/NonexistentParentClassType.php +++ b/src/Type/NonexistentParentClassType.php @@ -8,11 +8,14 @@ use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Traits\NonCallableTypeTrait; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonOffsetAccessibleTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\TruthyBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; @@ -26,6 +29,8 @@ class NonexistentParentClassType implements Type use TruthyBooleanTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonTypeTrait; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; public function describe(VerbosityLevel $level): string { @@ -44,12 +49,12 @@ public function hasProperty(string $propertyName): TrinaryLogic public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function canCallMethods(): TrinaryLogic @@ -64,12 +69,12 @@ public function hasMethod(string $methodName): TrinaryLogic public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function canAccessConstants(): TrinaryLogic @@ -84,7 +89,7 @@ public function hasConstant(string $constantName): TrinaryLogic public function getConstant(string $constantName): ConstantReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function isCloneable(): TrinaryLogic @@ -119,7 +124,6 @@ public function toArray(): Type /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { diff --git a/src/Type/NullType.php b/src/Type/NullType.php index 39b372f031..f86b990074 100644 --- a/src/Type/NullType.php +++ b/src/Type/NullType.php @@ -13,6 +13,7 @@ use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; /** @api */ class NullType implements ConstantScalarType @@ -23,6 +24,7 @@ class NullType implements ConstantScalarType use NonObjectTypeTrait; use FalseyBooleanTypeTrait; use NonGenericTypeTrait; + use NonRemoveableTypeTrait; /** @api */ public function __construct() @@ -45,7 +47,7 @@ public function getValue() return null; } - public function generalize(): Type + public function generalize(GeneralizePrecision $precision): Type { return $this; } @@ -57,7 +59,7 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic } if ($type instanceof CompoundType) { - return CompoundTypeHelper::accepts($type, $this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } return TrinaryLogic::createNo(); @@ -158,6 +160,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $array->setOffsetValueType($offsetType, $valueType, $unionValues); } + public function unsetOffset(Type $offsetType): Type + { + return $this; + } + public function traverse(callable $cb): Type { return $this; @@ -168,11 +175,26 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNumericString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function getSmallerType(): Type { return new NeverType(); @@ -211,7 +233,6 @@ public function getGreaterOrEqualType(): Type /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index bb5a6d75fd..eb0f926598 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -2,27 +2,48 @@ namespace PHPStan\Type; +use ArrayAccess; +use Closure; +use DateTime; +use DateTimeImmutable; +use DateTimeInterface; +use Iterator; +use IteratorAggregate; use PHPStan\Analyser\OutOfClassScope; use PHPStan\Broker\Broker; +use PHPStan\Broker\ClassNotFoundException; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\Php\UniversalObjectCratesClassReflectionExtension; use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\Reflection\Type\CalledOnTypeUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\CalledOnTypeUnresolvedPropertyPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Generic\GenericObjectType; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; +use Traversable; +use function array_keys; +use function array_map; +use function array_values; +use function count; +use function in_array; +use function sprintf; +use function strtolower; /** @api */ class ObjectType implements TypeWithClassName, SubtractableType @@ -30,16 +51,13 @@ class ObjectType implements TypeWithClassName, SubtractableType use NonGenericTypeTrait; use UndecidedComparisonTypeTrait; + use NonGeneralizableTypeTrait; private const EXTRA_OFFSET_CLASSES = ['SimpleXMLElement', 'DOMNodeList', 'Threaded']; - private string $className; + private ?Type $subtractedType; - private ?\PHPStan\Type\Type $subtractedType; - - private ?ClassReflection $classReflection; - - /** @var array> */ + /** @var array> */ private static array $superTypes = []; private ?self $cachedParent = null; @@ -61,18 +79,16 @@ class ObjectType implements TypeWithClassName, SubtractableType /** @api */ public function __construct( - string $className, + private string $className, ?Type $subtractedType = null, - ?ClassReflection $classReflection = null + private ?ClassReflection $classReflection = null, ) { if ($subtractedType instanceof NeverType) { $subtractedType = null; } - $this->className = $className; $this->subtractedType = $subtractedType; - $this->classReflection = $classReflection; } public static function resetCaches(): void @@ -91,7 +107,7 @@ private static function createFromReflection(ClassReflection $reflection): self return new GenericObjectType( $reflection->getName(), - $reflection->typeMapToList($reflection->getActiveTemplateTypeMap()) + $reflection->typeMapToList($reflection->getActiveTemplateTypeMap()), ); } @@ -138,7 +154,7 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember $nakedClassReflection = $this->getNakedClassReflection(); if ($nakedClassReflection === null) { - throw new \PHPStan\Broker\ClassNotFoundException($this->className); + throw new ClassNotFoundException($this->className); } if (!$nakedClassReflection->hasProperty($propertyName)) { @@ -146,7 +162,7 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember } if ($nakedClassReflection === null) { - throw new \PHPStan\Broker\ClassNotFoundException($this->className); + throw new ClassNotFoundException($this->className); } $property = $nakedClassReflection->getProperty($propertyName, $scope); @@ -167,7 +183,7 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember $property, $resolvedClassReflection, true, - $this + $this, ); } @@ -175,7 +191,7 @@ public function getPropertyWithoutTransformingStatic(string $propertyName, Class { $classReflection = $this->getNakedClassReflection(); if ($classReflection === null) { - throw new \PHPStan\Broker\ClassNotFoundException($this->className); + throw new ClassNotFoundException($this->className); } if (!$classReflection->hasProperty($propertyName)) { @@ -183,7 +199,7 @@ public function getPropertyWithoutTransformingStatic(string $propertyName, Class } if ($classReflection === null) { - throw new \PHPStan\Broker\ClassNotFoundException($this->className); + throw new ClassNotFoundException($this->className); } return $classReflection->getProperty($propertyName, $scope); @@ -200,15 +216,15 @@ public function getReferencedClasses(): array public function accepts(Type $type, bool $strictTypes): TrinaryLogic { if ($type instanceof StaticType) { - return $this->checkSubclassAcceptability($type->getBaseClass()); + return $this->checkSubclassAcceptability($type->getClassName()); } if ($type instanceof CompoundType) { - return CompoundTypeHelper::accepts($type, $this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } if ($type instanceof ClosureType) { - return $this->isInstanceOf(\Closure::class); + return $this->isInstanceOf(Closure::class); } if ($type instanceof ObjectWithoutClassType) { @@ -278,14 +294,14 @@ public function isSuperTypeOf(Type $type): TrinaryLogic return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createYes(); } - $broker = Broker::getInstance(); + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - if ($this->getClassReflection() === null || !$broker->hasClass($thatClassName)) { + if ($this->getClassReflection() === null || !$reflectionProvider->hasClass($thatClassName)) { return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createMaybe(); } $thisClassReflection = $this->getClassReflection(); - $thatClassReflection = $broker->getClass($thatClassName); + $thatClassReflection = $reflectionProvider->getClass($thatClassName); if ($thisClassReflection->getName() === $thatClassReflection->getName()) { return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createYes(); @@ -316,6 +332,10 @@ public function equals(Type $type): bool return false; } + if ($type instanceof EnumCaseObjectType) { + return false; + } + if ($this->className !== $type->className) { return false; } @@ -341,14 +361,14 @@ private function checkSubclassAcceptability(string $thatClass): TrinaryLogic return TrinaryLogic::createYes(); } - $broker = Broker::getInstance(); + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); - if ($this->getClassReflection() === null || !$broker->hasClass($thatClass)) { + if ($this->getClassReflection() === null || !$reflectionProvider->hasClass($thatClass)) { return TrinaryLogic::createNo(); } $thisReflection = $this->getClassReflection(); - $thatReflection = $broker->getClass($thatClass); + $thatReflection = $reflectionProvider->getClass($thatClass); if ($thisReflection->getName() === $thatReflection->getName()) { // class alias @@ -357,24 +377,24 @@ private function checkSubclassAcceptability(string $thatClass): TrinaryLogic if ($thisReflection->isInterface() && $thatReflection->isInterface()) { return TrinaryLogic::createFromBoolean( - $thatReflection->implementsInterface($this->className) + $thatReflection->implementsInterface($this->className), ); } return TrinaryLogic::createFromBoolean( - $thatReflection->isSubclassOf($this->className) + $thatReflection->isSubclassOf($this->className), ); } public function describe(VerbosityLevel $level): string { $preciseNameCallback = function (): string { - $broker = Broker::getInstance(); - if (!$broker->hasClass($this->className)) { + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + if (!$reflectionProvider->hasClass($this->className)) { return $this->className; } - return $broker->getClassName($this->className); + return $reflectionProvider->getClassName($this->className); }; $preciseWithSubtracted = function () use ($level): string { @@ -391,8 +411,16 @@ public function describe(VerbosityLevel $level): string $preciseNameCallback, $preciseWithSubtracted, function () use ($preciseWithSubtracted): string { - return $preciseWithSubtracted() . '-' . static::class . '-' . $this->describeAdditionalCacheKey(); - } + $reflection = $this->classReflection; + $line = ''; + if ($reflection !== null) { + $line .= '-'; + $line .= (string) $reflection->getNativeReflection()->getStartLine(); + $line .= '-'; + } + + return $preciseWithSubtracted() . '-' . static::class . '-' . $line . $this->describeAdditionalCacheKey(); + }, ); } @@ -412,6 +440,13 @@ private function describeCache(): string $description .= sprintf('~%s', $this->subtractedType->describe(VerbosityLevel::cache())); } + $reflection = $this->classReflection; + if ($reflection !== null) { + $description .= '-'; + $description .= (string) $reflection->getNativeReflection()->getStartLine(); + $description .= '-'; + } + return $description; } @@ -469,14 +504,14 @@ public function toArray(): Type return new ArrayType(new MixedType(), new MixedType()); } - $broker = Broker::getInstance(); + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); if ( !$classReflection->getNativeReflection()->isUserDefined() || UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate( - $broker, - $broker->getUniversalObjectCratesClasses(), - $classReflection + $reflectionProvider, + Broker::getInstance()->getUniversalObjectCratesClasses(), + $classReflection, ) ) { return new ArrayType(new MixedType(), new MixedType()); @@ -490,7 +525,7 @@ public function toArray(): Type continue; } - $declaringClass = $broker->getClass($nativeProperty->getDeclaringClass()->getName()); + $declaringClass = $reflectionProvider->getClass($nativeProperty->getDeclaringClass()->getName()); $property = $declaringClass->getNativeProperty($nativeProperty->getName()); $keyName = $nativeProperty->getName(); @@ -498,12 +533,12 @@ public function toArray(): Type $keyName = sprintf( "\0%s\0%s", $declaringClass->getName(), - $keyName + $keyName, ); } elseif ($nativeProperty->isProtected()) { $keyName = sprintf( "\0*\0%s", - $keyName + $keyName, ); } @@ -512,7 +547,7 @@ public function toArray(): Type } $classReflection = $classReflection->getParentClass(); - } while ($classReflection !== false); + } while ($classReflection !== null); return new ConstantArrayType($arrayKeys, $arrayValues); } @@ -577,7 +612,7 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce $nakedClassReflection = $this->getNakedClassReflection(); if ($nakedClassReflection === null) { - throw new \PHPStan\Broker\ClassNotFoundException($this->className); + throw new ClassNotFoundException($this->className); } if (!$nakedClassReflection->hasMethod($methodName)) { @@ -585,7 +620,7 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce } if ($nakedClassReflection === null) { - throw new \PHPStan\Broker\ClassNotFoundException($this->className); + throw new ClassNotFoundException($this->className); } $method = $nakedClassReflection->getMethod($methodName, $scope); @@ -606,7 +641,7 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce $method, $resolvedClassReflection, true, - $this + $this, ); } @@ -623,7 +658,7 @@ public function hasConstant(string $constantName): TrinaryLogic } return TrinaryLogic::createFromBoolean( - $class->hasConstant($constantName) + $class->hasConstant($constantName), ); } @@ -631,7 +666,7 @@ public function getConstant(string $constantName): ConstantReflection { $class = $this->getClassReflection(); if ($class === null) { - throw new \PHPStan\Broker\ClassNotFoundException($this->className); + throw new ClassNotFoundException($this->className); } return $class->getConstant($constantName); @@ -639,43 +674,55 @@ public function getConstant(string $constantName): ConstantReflection public function isIterable(): TrinaryLogic { - return $this->isInstanceOf(\Traversable::class); + return $this->isInstanceOf(Traversable::class); } public function isIterableAtLeastOnce(): TrinaryLogic { - return $this->isInstanceOf(\Traversable::class) + return $this->isInstanceOf(Traversable::class) ->and(TrinaryLogic::createMaybe()); } public function getIterableKeyType(): Type { - $classReflection = $this->getClassReflection(); - if ($classReflection === null) { - return new ErrorType(); + $isTraversable = false; + if ($this->isInstanceOf(Traversable::class)->yes()) { + $isTraversable = true; + $tKey = GenericTypeVariableResolver::getType($this, Traversable::class, 'TKey'); + if ($tKey !== null) { + if (!$tKey instanceof MixedType || $tKey->isExplicitMixed()) { + $classReflection = $this->getClassReflection(); + if ($classReflection === null) { + return $tKey; + } + + return TypeTraverser::map($tKey, static function (Type $type, callable $traverse) use ($classReflection): Type { + if ($type instanceof StaticType) { + return $type->changeBaseClass($classReflection)->getStaticObjectType(); + } + + return $traverse($type); + }); + } + } } - - if ($this->isInstanceOf(\Iterator::class)->yes()) { - return ParametersAcceptorSelector::selectSingle($this->getMethod('key', new OutOfClassScope())->getVariants())->getReturnType(); + if ($this->isInstanceOf(Iterator::class)->yes()) { + return RecursionGuard::run($this, fn (): Type => ParametersAcceptorSelector::selectSingle( + $this->getMethod('key', new OutOfClassScope())->getVariants(), + )->getReturnType()); } - if ($this->isInstanceOf(\IteratorAggregate::class)->yes()) { - $keyType = RecursionGuard::run($this, function (): Type { - return ParametersAcceptorSelector::selectSingle( - $this->getMethod('getIterator', new OutOfClassScope())->getVariants() - )->getReturnType()->getIterableKeyType(); - }); + if ($this->isInstanceOf(IteratorAggregate::class)->yes()) { + $keyType = RecursionGuard::run($this, fn (): Type => ParametersAcceptorSelector::selectSingle( + $this->getMethod('getIterator', new OutOfClassScope())->getVariants(), + )->getReturnType()->getIterableKeyType()); + $isTraversable = true; if (!$keyType instanceof MixedType || $keyType->isExplicitMixed()) { return $keyType; } } - if ($this->isInstanceOf(\Traversable::class)->yes()) { - $tKey = GenericTypeVariableResolver::getType($this, \Traversable::class, 'TKey'); - if ($tKey !== null) { - return $tKey; - } - + if ($isTraversable) { return new MixedType(); } @@ -684,29 +731,45 @@ public function getIterableKeyType(): Type public function getIterableValueType(): Type { - if ($this->isInstanceOf(\Iterator::class)->yes()) { - return ParametersAcceptorSelector::selectSingle( - $this->getMethod('current', new OutOfClassScope())->getVariants() - )->getReturnType(); + $isTraversable = false; + if ($this->isInstanceOf(Traversable::class)->yes()) { + $isTraversable = true; + $tValue = GenericTypeVariableResolver::getType($this, Traversable::class, 'TValue'); + if ($tValue !== null) { + if (!$tValue instanceof MixedType || $tValue->isExplicitMixed()) { + $classReflection = $this->getClassReflection(); + if ($classReflection === null) { + return $tValue; + } + + return TypeTraverser::map($tValue, static function (Type $type, callable $traverse) use ($classReflection): Type { + if ($type instanceof StaticType) { + return $type->changeBaseClass($classReflection)->getStaticObjectType(); + } + + return $traverse($type); + }); + } + } } - if ($this->isInstanceOf(\IteratorAggregate::class)->yes()) { - $valueType = RecursionGuard::run($this, function (): Type { - return ParametersAcceptorSelector::selectSingle( - $this->getMethod('getIterator', new OutOfClassScope())->getVariants() - )->getReturnType()->getIterableValueType(); - }); + if ($this->isInstanceOf(Iterator::class)->yes()) { + return RecursionGuard::run($this, fn (): Type => ParametersAcceptorSelector::selectSingle( + $this->getMethod('current', new OutOfClassScope())->getVariants(), + )->getReturnType()); + } + + if ($this->isInstanceOf(IteratorAggregate::class)->yes()) { + $valueType = RecursionGuard::run($this, fn (): Type => ParametersAcceptorSelector::selectSingle( + $this->getMethod('getIterator', new OutOfClassScope())->getVariants(), + )->getReturnType()->getIterableValueType()); + $isTraversable = true; if (!$valueType instanceof MixedType || $valueType->isExplicitMixed()) { return $valueType; } } - if ($this->isInstanceOf(\Traversable::class)->yes()) { - $tValue = GenericTypeVariableResolver::getType($this, \Traversable::class, 'TValue'); - if ($tValue !== null) { - return $tValue; - } - + if ($isTraversable) { return new MixedType(); } @@ -718,11 +781,26 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNumericString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + private function isExtraOffsetAccessibleClass(): TrinaryLogic { $classReflection = $this->getClassReflection(); @@ -752,21 +830,21 @@ private function isExtraOffsetAccessibleClass(): TrinaryLogic public function isOffsetAccessible(): TrinaryLogic { - return $this->isInstanceOf(\ArrayAccess::class)->or( - $this->isExtraOffsetAccessibleClass() + return $this->isInstanceOf(ArrayAccess::class)->or( + $this->isExtraOffsetAccessibleClass(), ); } public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - if ($this->isInstanceOf(\ArrayAccess::class)->yes()) { + if ($this->isInstanceOf(ArrayAccess::class)->yes()) { $acceptedOffsetType = RecursionGuard::run($this, function (): Type { $parameters = ParametersAcceptorSelector::selectSingle($this->getMethod('offsetSet', new OutOfClassScope())->getVariants())->getParameters(); if (count($parameters) < 2) { - throw new \PHPStan\ShouldNotHappenException(sprintf( + throw new ShouldNotHappenException(sprintf( 'Method %s::%s() has less than 2 parameters.', $this->className, - 'offsetSet' + 'offsetSet', )); } @@ -792,10 +870,8 @@ public function getOffsetValueType(Type $offsetType): Type return new MixedType(); } - if ($this->isInstanceOf(\ArrayAccess::class)->yes()) { - return RecursionGuard::run($this, function (): Type { - return ParametersAcceptorSelector::selectSingle($this->getMethod('offsetGet', new OutOfClassScope())->getVariants())->getReturnType(); - }); + if ($this->isInstanceOf(ArrayAccess::class)->yes()) { + return RecursionGuard::run($this, fn (): Type => ParametersAcceptorSelector::selectSingle($this->getMethod('offsetGet', new OutOfClassScope())->getVariants())->getReturnType()); } return new ErrorType(); @@ -807,15 +883,15 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return new ErrorType(); } - if ($this->isInstanceOf(\ArrayAccess::class)->yes()) { + if ($this->isInstanceOf(ArrayAccess::class)->yes()) { $acceptedValueType = new NeverType(); $acceptedOffsetType = RecursionGuard::run($this, function () use (&$acceptedValueType): Type { $parameters = ParametersAcceptorSelector::selectSingle($this->getMethod('offsetSet', new OutOfClassScope())->getVariants())->getParameters(); if (count($parameters) < 2) { - throw new \PHPStan\ShouldNotHappenException(sprintf( + throw new ShouldNotHappenException(sprintf( 'Method %s::%s() has less than 2 parameters.', $this->className, - 'offsetSet' + 'offsetSet', )); } @@ -841,6 +917,15 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } + public function unsetOffset(Type $offsetType): Type + { + if ($this->isOffsetAccessible()->no()) { + return new ErrorType(); + } + + return $this; + } + public function isCallable(): TrinaryLogic { $parametersAcceptors = $this->findCallableParametersAcceptors(); @@ -859,24 +944,23 @@ public function isCallable(): TrinaryLogic } /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] + * @return ParametersAcceptor[] */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { - if ($this->className === \Closure::class) { + if ($this->className === Closure::class) { return [new TrivialParametersAcceptor()]; } $parametersAcceptors = $this->findCallableParametersAcceptors(); if ($parametersAcceptors === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $parametersAcceptors; } /** - * @return \PHPStan\Reflection\ParametersAcceptor[]|null + * @return ParametersAcceptor[]|null */ private function findCallableParametersAcceptors(): ?array { @@ -903,13 +987,12 @@ public function isCloneable(): TrinaryLogic /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { return new self( $properties['className'], - $properties['subtractedType'] ?? null + $properties['subtractedType'] ?? null, ); } @@ -947,6 +1030,37 @@ public function getTypeWithoutSubtractedType(): Type public function changeSubtractedType(?Type $subtractedType): Type { + $classReflection = $this->getClassReflection(); + if ($classReflection !== null && $classReflection->isEnum() && $subtractedType !== null) { + $cases = []; + foreach (array_keys($classReflection->getEnumCases()) as $name) { + $cases[$name] = new EnumCaseObjectType($classReflection->getName(), $name); + } + + foreach (TypeUtils::flattenTypes($subtractedType) as $subType) { + if (!$subType instanceof EnumCaseObjectType) { + return new self($this->className, $subtractedType); + } + + if ($subType->getClassName() !== $this->getClassName()) { + return new self($this->className, $subtractedType); + } + + unset($cases[$subType->getEnumCaseName()]); + } + + $cases = array_values($cases); + if (count($cases) === 0) { + return new NeverType(); + } + + if (count($cases) === 1) { + return $cases[0]; + } + + return new UnionType(array_values($cases)); + } + return new self($this->className, $subtractedType); } @@ -962,7 +1076,7 @@ public function traverse(callable $cb): Type if ($subtractedType !== $this->subtractedType) { return new self( $this->className, - $subtractedType + $subtractedType, ); } @@ -974,14 +1088,13 @@ public function getNakedClassReflection(): ?ClassReflection if ($this->classReflection !== null) { return $this->classReflection; } - $broker = Broker::getInstance(); - if (!$broker->hasClass($this->className)) { + + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + if (!$reflectionProvider->hasClass($this->className)) { return null; } - $this->classReflection = $broker->getClass($this->className); - - return $this->classReflection; + return $reflectionProvider->getClass($this->className); } public function getClassReflection(): ?ClassReflection @@ -989,21 +1102,21 @@ public function getClassReflection(): ?ClassReflection if ($this->classReflection !== null) { return $this->classReflection; } - $broker = Broker::getInstance(); - if (!$broker->hasClass($this->className)) { + + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + if (!$reflectionProvider->hasClass($this->className)) { return null; } - $classReflection = $broker->getClass($this->className); + $classReflection = $reflectionProvider->getClass($this->className); if ($classReflection->isGeneric()) { - return $this->classReflection = $classReflection->withTypes(array_values($classReflection->getTemplateTypeMap()->resolveToBounds()->getTypes())); + return $classReflection->withTypes(array_values($classReflection->getTemplateTypeMap()->map(static fn (): Type => new ErrorType())->getTypes())); } - return $this->classReflection = $classReflection; + return $classReflection; } /** - * @param string $className * @return self|null */ public function getAncestorWithClassName(string $className): ?TypeWithClassName @@ -1022,11 +1135,11 @@ public function getAncestorWithClassName(string $className): ?TypeWithClassName return self::$ancestors[$description][$className]; } - $broker = Broker::getInstance(); - if (!$broker->hasClass($className)) { + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + if (!$reflectionProvider->hasClass($className)) { return null; } - $theirReflection = $broker->getClass($className); + $theirReflection = $reflectionProvider->getClass($className); if ($theirReflection->getName() === $thisReflection->getName()) { return self::$ancestors[$description][$className] = $this->currentAncestors[$className] = $this; @@ -1061,7 +1174,7 @@ private function getParent(): ?ObjectType } $parentReflection = $thisReflection->getParentClass(); - if ($parentReflection === false) { + if ($parentReflection === null) { return null; } @@ -1079,9 +1192,26 @@ private function getInterfaces(): array return $this->cachedInterfaces = []; } - return $this->cachedInterfaces = array_map(static function (ClassReflection $interfaceReflection): self { - return self::createFromReflection($interfaceReflection); - }, $thisReflection->getInterfaces()); + return $this->cachedInterfaces = array_map(static fn (ClassReflection $interfaceReflection): self => self::createFromReflection($interfaceReflection), $thisReflection->getInterfaces()); + } + + public function tryRemove(Type $typeToRemove): ?Type + { + if ($this->getClassName() === DateTimeInterface::class) { + if ($typeToRemove instanceof ObjectType && $typeToRemove->getClassName() === DateTimeImmutable::class) { + return new ObjectType(DateTime::class); + } + + if ($typeToRemove instanceof ObjectType && $typeToRemove->getClassName() === DateTime::class) { + return new ObjectType(DateTimeImmutable::class); + } + } + + if ($this->isSuperTypeOf($typeToRemove)->yes()) { + return $this->subtract($typeToRemove); + } + + return null; } } diff --git a/src/Type/ObjectWithoutClassType.php b/src/Type/ObjectWithoutClassType.php index bca31f1660..b602f4651b 100644 --- a/src/Type/ObjectWithoutClassType.php +++ b/src/Type/ObjectWithoutClassType.php @@ -3,9 +3,11 @@ namespace PHPStan\Type; use PHPStan\TrinaryLogic; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\ObjectTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; +use function sprintf; /** @api */ class ObjectWithoutClassType implements SubtractableType @@ -14,12 +16,13 @@ class ObjectWithoutClassType implements SubtractableType use ObjectTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonTypeTrait; + use NonGeneralizableTypeTrait; - private ?\PHPStan\Type\Type $subtractedType; + private ?Type $subtractedType; /** @api */ public function __construct( - ?Type $subtractedType = null + ?Type $subtractedType = null, ) { if ($subtractedType instanceof NeverType) { @@ -40,11 +43,11 @@ public function getReferencedClasses(): array public function accepts(Type $type, bool $strictTypes): TrinaryLogic { if ($type instanceof CompoundType) { - return CompoundTypeHelper::accepts($type, $this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } return TrinaryLogic::createFromBoolean( - $type instanceof self || $type instanceof TypeWithClassName + $type instanceof self || $type instanceof TypeWithClassName, ); } @@ -103,12 +106,8 @@ public function equals(Type $type): bool public function describe(VerbosityLevel $level): string { return $level->handle( - static function (): string { - return 'object'; - }, - static function (): string { - return 'object'; - }, + static fn (): string => 'object', + static fn (): string => 'object', function () use ($level): string { $description = 'object'; if ($this->subtractedType !== null) { @@ -116,7 +115,7 @@ function () use ($level): string { } return $description; - } + }, ); } @@ -159,9 +158,17 @@ public function traverse(callable $cb): Type return $this; } + public function tryRemove(Type $typeToRemove): ?Type + { + if ($this->isSuperTypeOf($typeToRemove)->yes()) { + return $this->subtract($typeToRemove); + } + + return null; + } + /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { diff --git a/src/Type/OperatorTypeSpecifyingExtensionRegistry.php b/src/Type/OperatorTypeSpecifyingExtensionRegistry.php index a1624cbe5e..9061500cf2 100644 --- a/src/Type/OperatorTypeSpecifyingExtensionRegistry.php +++ b/src/Type/OperatorTypeSpecifyingExtensionRegistry.php @@ -4,19 +4,18 @@ use PHPStan\Broker\Broker; use PHPStan\Reflection\BrokerAwareExtension; +use function array_filter; +use function array_values; class OperatorTypeSpecifyingExtensionRegistry { - /** @var OperatorTypeSpecifyingExtension[] */ - private array $extensions; - /** - * @param \PHPStan\Type\OperatorTypeSpecifyingExtension[] $extensions + * @param OperatorTypeSpecifyingExtension[] $extensions */ public function __construct( Broker $broker, - array $extensions + private array $extensions, ) { foreach ($extensions as $extension) { @@ -26,7 +25,6 @@ public function __construct( $extension->setBroker($broker); } - $this->extensions = $extensions; } /** @@ -34,9 +32,7 @@ public function __construct( */ public function getOperatorTypeSpecifyingExtensions(string $operator, Type $leftType, Type $rightType): array { - return array_values(array_filter($this->extensions, static function (OperatorTypeSpecifyingExtension $extension) use ($operator, $leftType, $rightType): bool { - return $extension->isOperatorSupported($operator, $leftType, $rightType); - })); + return array_values(array_filter($this->extensions, static fn (OperatorTypeSpecifyingExtension $extension): bool => $extension->isOperatorSupported($operator, $leftType, $rightType))); } } diff --git a/src/Type/ParserNodeTypeToPHPStanType.php b/src/Type/ParserNodeTypeToPHPStanType.php index a6910b65ae..c55b6ba2fe 100644 --- a/src/Type/ParserNodeTypeToPHPStanType.php +++ b/src/Type/ParserNodeTypeToPHPStanType.php @@ -2,47 +2,67 @@ namespace PHPStan\Type; +use PhpParser\Node; +use PhpParser\Node\Identifier; use PhpParser\Node\Name; use PhpParser\Node\NullableType; +use PHPStan\Reflection\ClassReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantBooleanType; +use function get_class; +use function in_array; +use function strtolower; class ParserNodeTypeToPHPStanType { /** - * @param \PhpParser\Node\Name|\PhpParser\Node\Identifier|\PhpParser\Node\NullableType|\PhpParser\Node\UnionType|null $type - * @param string|null $className - * @return Type + * @param Node\Name|Node\Identifier|Node\ComplexType|null $type */ - public static function resolve($type, ?string $className): Type + public static function resolve($type, ?ClassReflection $classReflection): Type { if ($type === null) { return new MixedType(); } elseif ($type instanceof Name) { $typeClassName = (string) $type; $lowercasedClassName = strtolower($typeClassName); - if ($className !== null && in_array($lowercasedClassName, ['self', 'static'], true)) { - $typeClassName = $className; + if ($classReflection !== null && in_array($lowercasedClassName, ['self', 'static'], true)) { + if ($lowercasedClassName === 'static') { + return new StaticType($classReflection); + } + $typeClassName = $classReflection->getName(); } elseif ( $lowercasedClassName === 'parent' + && $classReflection !== null + && $classReflection->getParentClass() !== null ) { - throw new \PHPStan\ShouldNotHappenException('parent type is not supported here'); - } - - if ($lowercasedClassName === 'static') { - return new StaticType($typeClassName); + $typeClassName = $classReflection->getParentClass()->getName(); } return new ObjectType($typeClassName); } elseif ($type instanceof NullableType) { - return TypeCombinator::addNull(self::resolve($type->type, $className)); - } elseif ($type instanceof \PhpParser\Node\UnionType) { + return TypeCombinator::addNull(self::resolve($type->type, $classReflection)); + } elseif ($type instanceof Node\UnionType) { $types = []; foreach ($type->types as $unionTypeType) { - $types[] = self::resolve($unionTypeType, $className); + $types[] = self::resolve($unionTypeType, $classReflection); } return TypeCombinator::union(...$types); + } elseif ($type instanceof Node\IntersectionType) { + $types = []; + foreach ($type->types as $intersectionTypeType) { + $innerType = self::resolve($intersectionTypeType, $classReflection); + if (!$innerType instanceof ObjectType) { + return new NeverType(); + } + + $types[] = $innerType; + } + + return TypeCombinator::intersect(...$types); + } elseif (!$type instanceof Identifier) { + throw new ShouldNotHappenException(get_class($type)); } $type = $type->name; @@ -70,6 +90,8 @@ public static function resolve($type, ?string $className): Type return new NullType(); } elseif ($type === 'mixed') { return new MixedType(true); + } elseif ($type === 'never') { + return new NeverType(true); } return new MixedType(); diff --git a/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php b/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php index 29ede2987c..2149ad66d2 100644 --- a/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php @@ -7,10 +7,11 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\ArrayType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\Type; -use PHPStan\Type\TypeUtils; -class ArgumentBasedFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class ArgumentBasedFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { /** @var int[] */ @@ -33,7 +34,6 @@ class ArgumentBasedFunctionReturnTypeExtension implements \PHPStan\Type\DynamicF 'array_uintersect_assoc' => 0, 'array_uintersect_uassoc' => 0, 'array_uintersect' => 0, - 'iterator_to_array' => 0, ]; public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -45,22 +45,22 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, { $argumentPosition = $this->functionNames[$functionReflection->getName()]; - if (!isset($functionCall->args[$argumentPosition])) { + if (!isset($functionCall->getArgs()[$argumentPosition])) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } - $argument = $functionCall->args[$argumentPosition]; + $argument = $functionCall->getArgs()[$argumentPosition]; $argumentType = $scope->getType($argument->value); $argumentKeyType = $argumentType->getIterableKeyType(); $argumentValueType = $argumentType->getIterableValueType(); if ($argument->unpack) { - $argumentKeyType = TypeUtils::generalizeType($argumentKeyType); - $argumentValueType = TypeUtils::generalizeType($argumentValueType->getIterableValueType()); + $argumentKeyType = $argumentKeyType->generalize(GeneralizePrecision::moreSpecific()); + $argumentValueType = $argumentValueType->getIterableValueType()->generalize(GeneralizePrecision::moreSpecific()); } return new ArrayType( $argumentKeyType, - $argumentValueType + $argumentValueType, ); } diff --git a/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php b/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..f044cc5f8f --- /dev/null +++ b/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php @@ -0,0 +1,208 @@ +getName() === 'array_column'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + { + $numArgs = count($functionCall->getArgs()); + if ($numArgs < 2) { + return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + } + + $arrayType = $scope->getType($functionCall->getArgs()[0]->value); + $columnType = $scope->getType($functionCall->getArgs()[1]->value); + $indexType = $numArgs >= 3 ? $scope->getType($functionCall->getArgs()[2]->value) : null; + + $constantArrayTypes = TypeUtils::getConstantArrays($arrayType); + if (count($constantArrayTypes) === 1) { + $type = $this->handleConstantArray($constantArrayTypes[0], $columnType, $indexType, $scope); + if ($type !== null) { + return $type; + } + } + + return $this->handleAnyArray($arrayType, $columnType, $indexType, $scope); + } + + private function handleAnyArray(Type $arrayType, Type $columnType, ?Type $indexType, Scope $scope): Type + { + $iterableAtLeastOnce = $arrayType->isIterableAtLeastOnce(); + if ($iterableAtLeastOnce->no()) { + return new ConstantArrayType([], []); + } + + $iterableValueType = $arrayType->getIterableValueType(); + $returnValueType = $this->getOffsetOrProperty($iterableValueType, $columnType, $scope, false); + + if ($returnValueType === null) { + $returnValueType = $this->getOffsetOrProperty($iterableValueType, $columnType, $scope, true); + $iterableAtLeastOnce = TrinaryLogic::createMaybe(); + if ($returnValueType === null) { + throw new ShouldNotHappenException(); + } + } + + if ($returnValueType instanceof NeverType) { + return new ConstantArrayType([], []); + } + + if ($indexType !== null) { + $type = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope, false); + if ($type !== null) { + $returnKeyType = $type; + } else { + $type = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope, true); + if ($type !== null) { + $returnKeyType = TypeCombinator::union($type, new IntegerType()); + } else { + $returnKeyType = new IntegerType(); + } + } + } else { + $returnKeyType = new IntegerType(); + } + + $returnType = new ArrayType($this->castToArrayKeyType($returnKeyType), $returnValueType); + + if ($iterableAtLeastOnce->yes()) { + $returnType = TypeCombinator::intersect($returnType, new NonEmptyArrayType()); + } + + return $returnType; + } + + private function handleConstantArray(ConstantArrayType $arrayType, Type $columnType, ?Type $indexType, Scope $scope): ?Type + { + $builder = ConstantArrayTypeBuilder::createEmpty(); + + foreach ($arrayType->getValueTypes() as $iterableValueType) { + $valueType = $this->getOffsetOrProperty($iterableValueType, $columnType, $scope, false); + if ($valueType === null) { + return null; + } + + if ($indexType !== null) { + $type = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope, false); + if ($type !== null) { + $keyType = $type; + } else { + $type = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope, true); + if ($type !== null) { + $keyType = TypeCombinator::union($type, new IntegerType()); + } else { + $keyType = null; + } + } + } else { + $keyType = null; + } + + if ($keyType !== null) { + $keyType = $this->castToArrayKeyType($keyType); + } + $builder->setOffsetValueType($keyType, $valueType); + } + + return $builder->getArray(); + } + + private function getOffsetOrProperty(Type $type, Type $offsetOrProperty, Scope $scope, bool $allowMaybe): ?Type + { + $offsetIsNull = (new NullType())->isSuperTypeOf($offsetOrProperty); + if ($offsetIsNull->yes()) { + return $type; + } + + $returnTypes = []; + + if ($offsetIsNull->maybe()) { + $returnTypes[] = $type; + } + + if (!$type->canAccessProperties()->no()) { + $propertyTypes = TypeUtils::getConstantStrings($offsetOrProperty); + if ($propertyTypes === []) { + return new MixedType(); + } + foreach ($propertyTypes as $propertyType) { + $propertyName = $propertyType->getValue(); + $hasProperty = $type->hasProperty($propertyName); + if ($hasProperty->maybe()) { + return $allowMaybe ? new MixedType() : null; + } + if (!$hasProperty->yes()) { + continue; + } + + $returnTypes[] = $type->getProperty($propertyName, $scope)->getReadableType(); + } + } + + if ($type->isOffsetAccessible()->yes()) { + $hasOffset = $type->hasOffsetValueType($offsetOrProperty); + if (!$allowMaybe && $hasOffset->maybe()) { + return null; + } + if (!$hasOffset->no()) { + $returnTypes[] = $type->getOffsetValueType($offsetOrProperty); + } + } + + if ($returnTypes === []) { + return new NeverType(); + } + + return TypeCombinator::union(...$returnTypes); + } + + private function castToArrayKeyType(Type $type): Type + { + $isArray = $type->isArray(); + if ($isArray->yes()) { + return $this->phpVersion->throwsTypeErrorForInternalFunctions() ? new NeverType() : new IntegerType(); + } + if ($isArray->no()) { + return ArrayType::castToArrayKeyType($type); + } + $withoutArrayType = TypeCombinator::remove($type, new ArrayType(new MixedType(), new MixedType())); + $keyType = ArrayType::castToArrayKeyType($withoutArrayType); + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return $keyType; + } + return TypeCombinator::union($keyType, new IntegerType()); + } + +} diff --git a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php index d1cdd2df95..7b50fe74a2 100644 --- a/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php @@ -8,23 +8,24 @@ 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; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\MixedType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; +use function count; -class ArrayCombineFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class ArrayCombineFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - private PhpVersion $phpVersion; - - public function __construct(PhpVersion $phpVersion) + public function __construct(private PhpVersion $phpVersion) { - $this->phpVersion = $phpVersion; } public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -34,12 +35,12 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - if (count($functionCall->args) < 2) { + if (count($functionCall->getArgs()) < 2) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } - $firstArg = $functionCall->args[0]->value; - $secondArg = $functionCall->args[1]->value; + $firstArg = $functionCall->getArgs()[0]->value; + $secondArg = $functionCall->getArgs()[1]->value; $keysParamType = $scope->getType($firstArg); $valuesParamType = $scope->getType($secondArg); @@ -59,16 +60,20 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($keyTypes !== null) { return new ConstantArrayType( $keyTypes, - $valueTypes + $valueTypes, ); } } $arrayType = new ArrayType( $keysParamType instanceof ArrayType ? $keysParamType->getItemType() : new MixedType(), - $valuesParamType instanceof ArrayType ? $valuesParamType->getItemType() : new MixedType() + $valuesParamType instanceof ArrayType ? $valuesParamType->getItemType() : new MixedType(), ); + if ($keysParamType->isIterableAtLeastOnce()->yes() && $valuesParamType->isIterableAtLeastOnce()->yes()) { + $arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); + } + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { return $arrayType; } diff --git a/src/Type/Php/ArrayCurrentDynamicReturnTypeExtension.php b/src/Type/Php/ArrayCurrentDynamicReturnTypeExtension.php index a322e4de05..0830d0e3c6 100644 --- a/src/Type/Php/ArrayCurrentDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayCurrentDynamicReturnTypeExtension.php @@ -7,10 +7,11 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class ArrayCurrentDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class ArrayCurrentDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -20,11 +21,11 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - if (!isset($functionCall->args[0])) { + if (!isset($functionCall->getArgs()[0])) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } - $argType = $scope->getType($functionCall->args[0]->value); + $argType = $scope->getType($functionCall->getArgs()[0]->value); $iterableAtLeastOnce = $argType->isIterableAtLeastOnce(); if ($iterableAtLeastOnce->no()) { return new ConstantBooleanType(false); diff --git a/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php index 8b1819d57f..9456a3e92e 100644 --- a/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php @@ -4,21 +4,32 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\NeverType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use function count; -class ArrayFillFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class ArrayFillFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { private const MAX_SIZE_USE_CONSTANT_ARRAY = 100; + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_fill'; @@ -26,25 +37,42 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - if (count($functionCall->args) < 3) { + if (count($functionCall->getArgs()) < 3) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } - $startIndexType = $scope->getType($functionCall->args[0]->value); - $numberType = $scope->getType($functionCall->args[1]->value); - $valueType = $scope->getType($functionCall->args[2]->value); + $startIndexType = $scope->getType($functionCall->getArgs()[0]->value); + $numberType = $scope->getType($functionCall->getArgs()[1]->value); + $valueType = $scope->getType($functionCall->getArgs()[2]->value); + + if ($numberType instanceof IntegerRangeType) { + if ($numberType->getMin() < 0) { + return TypeCombinator::union( + new ArrayType(new IntegerType(), $valueType), + new ConstantBooleanType(false), + ); + } + } + + // check against negative-int, which is not allowed + if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($numberType)->yes()) { + if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { + return new NeverType(); + } + return new ConstantBooleanType(false); + } if ( $startIndexType instanceof ConstantIntegerType && $numberType instanceof ConstantIntegerType - && $numberType->getValue() <= static::MAX_SIZE_USE_CONSTANT_ARRAY + && $numberType->getValue() <= self::MAX_SIZE_USE_CONSTANT_ARRAY ) { $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); $nextIndex = $startIndexType->getValue(); for ($i = 0; $i < $numberType->getValue(); $i++) { $arrayBuilder->setOffsetValueType( new ConstantIntegerType($nextIndex), - $valueType + $valueType, ); if ($nextIndex < 0) { $nextIndex = 0; @@ -56,10 +84,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $arrayBuilder->getArray(); } - if ( - $numberType instanceof ConstantIntegerType - && $numberType->getValue() > 0 - ) { + if (IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($numberType)->yes()) { return new IntersectionType([ new ArrayType(new IntegerType(), $valueType), new NonEmptyArrayType(), diff --git a/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php index 28ffedb197..e3792521de 100644 --- a/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php @@ -8,11 +8,13 @@ use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function count; -class ArrayFillKeysFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class ArrayFillKeysFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -22,12 +24,12 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - if (count($functionCall->args) < 2) { + if (count($functionCall->getArgs()) < 2) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } - $valueType = $scope->getType($functionCall->args[1]->value); - $keysType = $scope->getType($functionCall->args[0]->value); + $valueType = $scope->getType($functionCall->getArgs()[1]->value); + $keysType = $scope->getType($functionCall->getArgs()[0]->value); $constantArrays = TypeUtils::getConstantArrays($keysType); if (count($constantArrays) === 0) { return new ArrayType($keysType->getIterableValueType(), $valueType); diff --git a/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php b/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php index 0876a38dc9..bf0eb42757 100644 --- a/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php +++ b/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php @@ -2,18 +2,26 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Arg; +use PhpParser\Node\Expr; use PhpParser\Node\Expr\ArrowFunction; use PhpParser\Node\Expr\Closure; +use PhpParser\Node\Expr\ConstFetch; +use PhpParser\Node\Expr\Error; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\Variable; +use PhpParser\Node\Name; +use PhpParser\Node\Scalar\String_; use PhpParser\Node\Stmt\Return_; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\ArrayType; use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\NullType; @@ -21,8 +29,12 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function array_map; +use function count; +use function is_string; +use function strtolower; -class ArrayFilterFunctionReturnTypeReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class ArrayFilterFunctionReturnTypeReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -32,58 +44,79 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - $arrayArg = $functionCall->args[0]->value ?? null; - $callbackArg = $functionCall->args[1]->value ?? null; - $flagArg = $functionCall->args[2]->value ?? null; - - if ($arrayArg !== null) { - $arrayArgType = $scope->getType($arrayArg); - $keyType = $arrayArgType->getIterableKeyType(); - $itemType = $arrayArgType->getIterableValueType(); - - if ($arrayArgType instanceof MixedType) { - return new BenevolentUnionType([ - new ArrayType(new MixedType(), new MixedType()), - new NullType(), - ]); - } + $arrayArg = $functionCall->getArgs()[0]->value ?? null; + $callbackArg = $functionCall->getArgs()[1]->value ?? null; + $flagArg = $functionCall->getArgs()[2]->value ?? null; + + if ($arrayArg === null) { + return new ArrayType(new MixedType(), new MixedType()); + } + + $arrayArgType = $scope->getType($arrayArg); + $keyType = $arrayArgType->getIterableKeyType(); + $itemType = $arrayArgType->getIterableValueType(); + + if ($arrayArgType instanceof MixedType) { + return new BenevolentUnionType([ + new ArrayType(new MixedType(), new MixedType()), + new NullType(), + ]); + } + + if ($callbackArg === null || ($callbackArg instanceof ConstFetch && strtolower($callbackArg->name->parts[0]) === 'null')) { + return TypeCombinator::union( + ...array_map([$this, 'removeFalsey'], TypeUtils::getArrays($arrayArgType)), + ); + } - if ($callbackArg === null) { - return TypeCombinator::union( - ...array_map([$this, 'removeFalsey'], TypeUtils::getArrays($arrayArgType)) - ); + if ($flagArg === null) { + if ($callbackArg instanceof Closure && count($callbackArg->stmts) === 1 && count($callbackArg->params) > 0) { + $statement = $callbackArg->stmts[0]; + if ($statement instanceof Return_ && $statement->expr !== null) { + [$itemType, $keyType] = $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $itemType, null, $keyType, $statement->expr); + } + } elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) { + [$itemType, $keyType] = $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $itemType, null, $keyType, $callbackArg->expr); + } elseif ($callbackArg instanceof String_) { + $itemVar = new Variable('item'); + $expr = new FuncCall(new Name($callbackArg->value), [new Arg($itemVar)]); + [$itemType, $keyType] = $this->filterByTruthyValue($scope, $itemVar, $itemType, null, $keyType, $expr); } + } - if ($flagArg === null) { - $var = null; - $expr = null; - if ($callbackArg instanceof Closure && count($callbackArg->stmts) === 1 && count($callbackArg->params) > 0) { - $statement = $callbackArg->stmts[0]; - if ($statement instanceof Return_ && $statement->expr !== null) { - $var = $callbackArg->params[0]->var; - $expr = $statement->expr; - } - } elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) { - $var = $callbackArg->params[0]->var; - $expr = $callbackArg->expr; + if ($flagArg instanceof ConstFetch && $flagArg->name->parts[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) { + [$itemType, $keyType] = $this->filterByTruthyValue($scope, null, $itemType, $callbackArg->params[0]->var, $keyType, $statement->expr); } - if ($var !== null && $expr !== null) { - if (!$var instanceof Variable || !is_string($var->name)) { - throw new \PHPStan\ShouldNotHappenException(); - } - $itemVariableName = $var->name; - if (!$scope instanceof MutatingScope) { - throw new \PHPStan\ShouldNotHappenException(); - } - $scope = $scope->assignVariable($itemVariableName, $itemType); - $scope = $scope->filterByTruthyValue($expr); - $itemType = $scope->getVariableType($itemVariableName); + } elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) { + [$itemType, $keyType] = $this->filterByTruthyValue($scope, null, $itemType, $callbackArg->params[0]->var, $keyType, $callbackArg->expr); + } elseif ($callbackArg instanceof String_) { + $keyVar = new Variable('key'); + $expr = new FuncCall(new Name($callbackArg->value), [new Arg($keyVar)]); + [$itemType, $keyType] = $this->filterByTruthyValue($scope, null, $itemType, $keyVar, $keyType, $expr); + } + } + + if ($flagArg instanceof ConstFetch && $flagArg->name->parts[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) { + [$itemType, $keyType] = $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $itemType, $callbackArg->params[1]->var ?? null, $keyType, $statement->expr); } + } elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) { + [$itemType, $keyType] = $this->filterByTruthyValue($scope, $callbackArg->params[0]->var, $itemType, $callbackArg->params[1]->var ?? null, $keyType, $callbackArg->expr); + } elseif ($callbackArg instanceof String_) { + $itemVar = new Variable('item'); + $keyVar = new Variable('key'); + $expr = new FuncCall(new Name($callbackArg->value), [new Arg($itemVar), new Arg($keyVar)]); + [$itemType, $keyType] = $this->filterByTruthyValue($scope, $itemVar, $itemType, $keyVar, $keyType, $expr); } + } - } else { - $keyType = new MixedType(); - $itemType = new MixedType(); + if ($itemType instanceof NeverType || $keyType instanceof NeverType) { + return new ConstantArrayType([], []); } return new ArrayType($keyType, $itemType); @@ -124,4 +157,39 @@ public function removeFalsey(Type $type): Type return new ArrayType($keyType, $valueType); } + /** + * @return array{Type, Type} + */ + private function filterByTruthyValue(Scope $scope, Error|Variable|null $itemVar, Type $itemType, Error|Variable|null $keyVar, Type $keyType, Expr $expr): array + { + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); + } + + $itemVarName = null; + if ($itemVar !== null) { + if (!$itemVar instanceof Variable || !is_string($itemVar->name)) { + throw new ShouldNotHappenException(); + } + $itemVarName = $itemVar->name; + $scope = $scope->assignVariable($itemVarName, $itemType); + } + + $keyVarName = null; + if ($keyVar !== null) { + if (!$keyVar instanceof Variable || !is_string($keyVar->name)) { + throw new ShouldNotHappenException(); + } + $keyVarName = $keyVar->name; + $scope = $scope->assignVariable($keyVarName, $keyType); + } + + $scope = $scope->filterByTruthyValue($expr); + + return [ + $itemVarName !== null ? $scope->getVariableType($itemVarName) : $itemType, + $keyVarName !== null ? $scope->getVariableType($keyVarName) : $keyType, + ]; + } + } diff --git a/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..71f1140218 --- /dev/null +++ b/src/Type/Php/ArrayFlipFunctionReturnTypeExtension.php @@ -0,0 +1,54 @@ +getName() === 'array_flip'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + { + if (count($functionCall->getArgs()) !== 1) { + return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + } + + $array = $functionCall->getArgs()[0]->value; + $argType = $scope->getType($array); + + if ($argType->isArray()->yes()) { + $keyType = $argType->getIterableKeyType(); + $itemType = $argType->getIterableValueType(); + + $itemType = ArrayType::castToArrayKeyType($itemType); + + $flippedArrayType = new ArrayType( + $itemType, + $keyType, + ); + + if ($argType->isIterableAtLeastOnce()->yes()) { + $flippedArrayType = TypeCombinator::intersect($flippedArrayType, new NonEmptyArrayType()); + } + + return $flippedArrayType; + } + + return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + } + +} diff --git a/src/Type/Php/ArrayIsListFunctionTypeSpecifyingExtension.php b/src/Type/Php/ArrayIsListFunctionTypeSpecifyingExtension.php new file mode 100644 index 0000000000..da9de1508c --- /dev/null +++ b/src/Type/Php/ArrayIsListFunctionTypeSpecifyingExtension.php @@ -0,0 +1,66 @@ +getName()) === 'array_is_list' + && !$context->null(); + } + + public function specifyTypes( + FunctionReflection $functionReflection, + FuncCall $node, + Scope $scope, + TypeSpecifierContext $context, + ): SpecifiedTypes + { + $arrayArg = $node->getArgs()[0]->value ?? null; + if ($arrayArg === null) { + return new SpecifiedTypes(); + } + + $valueType = $scope->getType($arrayArg); + if ($valueType instanceof ConstantArrayType) { + return $this->typeSpecifier->create($arrayArg, $valueType->getValuesArray(), $context, false, $scope); + } + + return $this->typeSpecifier->create( + $arrayArg, + TypeCombinator::intersect(new ArrayType(new IntegerType(), $valueType->getIterableValueType()), ...TypeUtils::getAccessoryTypes($valueType)), + $context, + false, + $scope, + ); + } + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + +} diff --git a/src/Type/Php/ArrayKeyDynamicReturnTypeExtension.php b/src/Type/Php/ArrayKeyDynamicReturnTypeExtension.php index 08cb3013dc..2a698e153f 100644 --- a/src/Type/Php/ArrayKeyDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeyDynamicReturnTypeExtension.php @@ -6,11 +6,12 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class ArrayKeyDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class ArrayKeyDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -20,11 +21,11 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - if (!isset($functionCall->args[0])) { + if (!isset($functionCall->getArgs()[0])) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } - $argType = $scope->getType($functionCall->args[0]->value); + $argType = $scope->getType($functionCall->getArgs()[0]->value); $iterableAtLeastOnce = $argType->isIterableAtLeastOnce(); if ($iterableAtLeastOnce->no()) { return new NullType(); diff --git a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php index 837f5e88a9..39213613fd 100644 --- a/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/ArrayKeyExistsFunctionTypeSpecifyingExtension.php @@ -14,11 +14,12 @@ use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\MixedType; use PHPStan\Type\TypeCombinator; +use function count; class ArrayKeyExistsFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void { @@ -28,7 +29,7 @@ public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void public function isFunctionSupported( FunctionReflection $functionReflection, FuncCall $node, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): bool { return $functionReflection->getName() === 'array_key_exists' @@ -39,29 +40,29 @@ public function specifyTypes( FunctionReflection $functionReflection, FuncCall $node, Scope $scope, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): SpecifiedTypes { - if (count($node->args) < 2) { + if (count($node->getArgs()) < 2) { return new SpecifiedTypes(); } - $keyType = $scope->getType($node->args[0]->value); + $keyType = $scope->getType($node->getArgs()[0]->value); if ($context->truthy()) { $type = TypeCombinator::intersect( new ArrayType(new MixedType(), new MixedType()), - new HasOffsetType($keyType) + new HasOffsetType($keyType), ); } else { $type = new HasOffsetType($keyType); } return $this->typeSpecifier->create( - $node->args[1]->value, + $node->getArgs()[1]->value, $type, $context, false, - $scope + $scope, ); } diff --git a/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php b/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php index c868e7a39e..1fa52f314f 100644 --- a/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeyFirstDynamicReturnTypeExtension.php @@ -6,12 +6,14 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function count; -class ArrayKeyFirstDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class ArrayKeyFirstDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -21,11 +23,11 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - if (!isset($functionCall->args[0])) { + if (!isset($functionCall->getArgs()[0])) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } - $argType = $scope->getType($functionCall->args[0]->value); + $argType = $scope->getType($functionCall->getArgs()[0]->value); $iterableAtLeastOnce = $argType->isIterableAtLeastOnce(); if ($iterableAtLeastOnce->no()) { return new NullType(); diff --git a/src/Type/Php/ArrayKeyLastDynamicReturnTypeExtension.php b/src/Type/Php/ArrayKeyLastDynamicReturnTypeExtension.php index 25bec78100..29f913b62d 100644 --- a/src/Type/Php/ArrayKeyLastDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeyLastDynamicReturnTypeExtension.php @@ -6,12 +6,14 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function count; -class ArrayKeyLastDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class ArrayKeyLastDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -21,11 +23,11 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - if (!isset($functionCall->args[0])) { + if (!isset($functionCall->getArgs()[0])) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } - $argType = $scope->getType($functionCall->args[0]->value); + $argType = $scope->getType($functionCall->getArgs()[0]->value); $iterableAtLeastOnce = $argType->isIterableAtLeastOnce(); if ($iterableAtLeastOnce->no()) { return new NullType(); diff --git a/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php index 056294ffc9..e0f0d451c8 100644 --- a/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayKeysFunctionDynamicReturnTypeExtension.php @@ -7,6 +7,7 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerType; use PHPStan\Type\StringType; use PHPStan\Type\Type; @@ -14,7 +15,7 @@ use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; -class ArrayKeysFunctionDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class ArrayKeysFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -24,7 +25,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - $arrayArg = $functionCall->args[0]->value ?? null; + $arrayArg = $functionCall->getArgs()[0]->value ?? null; if ($arrayArg !== null) { $valueType = $scope->getType($arrayArg); if ($valueType->isArray()->yes()) { @@ -38,7 +39,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new ArrayType( new IntegerType(), - new UnionType([new StringType(), new IntegerType()]) + new UnionType([new StringType(), new IntegerType()]), ); } diff --git a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php index e21cdbffe0..4284d4ee0e 100644 --- a/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayMapFunctionReturnTypeExtension.php @@ -6,14 +6,22 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; +use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function array_slice; +use function count; -class ArrayMapFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class ArrayMapFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -23,47 +31,76 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - if (count($functionCall->args) < 2) { + if (count($functionCall->getArgs()) < 2) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } - $valueType = new MixedType(); - $callableType = $scope->getType($functionCall->args[0]->value); - if (!$callableType->isCallable()->no()) { - $valueType = ParametersAcceptorSelector::selectFromArgs( - $scope, - $functionCall->args, - $callableType->getCallableParametersAcceptors($scope) - )->getReturnType(); + $singleArrayArgument = !isset($functionCall->getArgs()[2]); + $callableType = $scope->getType($functionCall->getArgs()[0]->value); + $callableIsNull = (new NullType())->isSuperTypeOf($callableType)->yes(); + + if ($callableType->isCallable()->yes()) { + $valueType = new NeverType(); + foreach ($callableType->getCallableParametersAcceptors($scope) as $parametersAcceptor) { + $valueType = TypeCombinator::union($valueType, $parametersAcceptor->getReturnType()); + } + } elseif ($callableIsNull) { + $arrayBuilder = ConstantArrayTypeBuilder::createEmpty(); + foreach (array_slice($functionCall->getArgs(), 1) as $index => $arg) { + $arrayBuilder->setOffsetValueType( + new ConstantIntegerType($index), + $scope->getType($arg->value)->getIterableValueType(), + ); + } + $valueType = $arrayBuilder->getArray(); + } else { + $valueType = new MixedType(); } - $arrayType = $scope->getType($functionCall->args[1]->value); - $constantArrays = TypeUtils::getConstantArrays($arrayType); - if (count($constantArrays) > 0) { - $arrayTypes = []; - foreach ($constantArrays as $constantArray) { - $returnedArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); - foreach ($constantArray->getKeyTypes() as $keyType) { - $returnedArrayBuilder->setOffsetValueType( - $keyType, - $valueType - ); - } - $arrayTypes[] = $returnedArrayBuilder->getArray(); + $arrayType = $scope->getType($functionCall->getArgs()[1]->value); + + if ($singleArrayArgument) { + if ($callableIsNull) { + return $arrayType; } + $constantArrays = TypeUtils::getConstantArrays($arrayType); + if (count($constantArrays) > 0) { + $arrayTypes = []; + foreach ($constantArrays as $constantArray) { + $returnedArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); + foreach ($constantArray->getKeyTypes() as $keyType) { + $returnedArrayBuilder->setOffsetValueType( + $keyType, + $valueType, + ); + } + $arrayTypes[] = $returnedArrayBuilder->getArray(); + } - return TypeCombinator::union(...$arrayTypes); - } elseif ($arrayType->isArray()->yes()) { - return TypeCombinator::intersect(new ArrayType( - $arrayType->getIterableKeyType(), - $valueType + $mappedArrayType = TypeCombinator::union(...$arrayTypes); + } elseif ($arrayType->isArray()->yes()) { + $mappedArrayType = TypeCombinator::intersect(new ArrayType( + $arrayType->getIterableKeyType(), + $valueType, + ), ...TypeUtils::getAccessoryTypes($arrayType)); + } else { + $mappedArrayType = new ArrayType( + new MixedType(), + $valueType, + ); + } + } else { + $mappedArrayType = TypeCombinator::intersect(new ArrayType( + new IntegerType(), + $valueType, ), ...TypeUtils::getAccessoryTypes($arrayType)); } - return new ArrayType( - new MixedType(), - $valueType - ); + if ($arrayType->isIterableAtLeastOnce()->yes()) { + $mappedArrayType = TypeCombinator::intersect($mappedArrayType, new NonEmptyArrayType()); + } + + return $mappedArrayType; } } diff --git a/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php index 38bb6b3141..4fcc3338bf 100644 --- a/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php @@ -6,13 +6,17 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; +use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\GeneralizePrecision; +use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; -class ArrayMergeFunctionDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class ArrayMergeFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -22,13 +26,14 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - if (!isset($functionCall->args[0])) { + if (!isset($functionCall->getArgs()[0])) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } $keyTypes = []; $valueTypes = []; - foreach ($functionCall->args as $arg) { + $nonEmpty = false; + foreach ($functionCall->getArgs() as $arg) { $argType = $scope->getType($arg->value); if ($arg->unpack) { $argType = $argType->getIterableValueType(); @@ -39,14 +44,31 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } } - $keyTypes[] = TypeUtils::generalizeType($argType->getIterableKeyType()); + $keyTypes[] = $argType->getIterableKeyType()->generalize(GeneralizePrecision::moreSpecific()); $valueTypes[] = $argType->getIterableValueType(); + + if (!$argType->isIterableAtLeastOnce()->yes()) { + continue; + } + + $nonEmpty = true; + } + + $keyType = TypeCombinator::union(...$keyTypes); + if ($keyType instanceof NeverType && !$keyType->isExplicit()) { + return new ConstantArrayType([], []); } - return new ArrayType( - TypeCombinator::union(...$keyTypes), - TypeCombinator::union(...$valueTypes) + $arrayType = new ArrayType( + $keyType, + TypeCombinator::union(...$valueTypes), ); + + if ($nonEmpty) { + $arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); + } + + return $arrayType; } } diff --git a/src/Type/Php/ArrayNextDynamicReturnTypeExtension.php b/src/Type/Php/ArrayNextDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..f5c0fed29c --- /dev/null +++ b/src/Type/Php/ArrayNextDynamicReturnTypeExtension.php @@ -0,0 +1,40 @@ +getName(), ['next', 'prev'], true); + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + { + if (!isset($functionCall->getArgs()[0])) { + return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + } + + $argType = $scope->getType($functionCall->getArgs()[0]->value); + $iterableAtLeastOnce = $argType->isIterableAtLeastOnce(); + if ($iterableAtLeastOnce->no()) { + return new ConstantBooleanType(false); + } + + $valueType = $argType->getIterableValueType(); + + return TypeCombinator::union($valueType, new ConstantBooleanType(false)); + } + +} diff --git a/src/Type/Php/ArrayPointerFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/ArrayPointerFunctionsDynamicReturnTypeExtension.php index b7e9557ded..798ded9529 100644 --- a/src/Type/Php/ArrayPointerFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayPointerFunctionsDynamicReturnTypeExtension.php @@ -7,11 +7,14 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function count; +use function in_array; -class ArrayPointerFunctionsDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class ArrayPointerFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { /** @var string[] */ @@ -28,14 +31,14 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope + Scope $scope, ): Type { - if (count($functionCall->args) === 0) { + if (count($functionCall->getArgs()) === 0) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } - $argType = $scope->getType($functionCall->args[0]->value); + $argType = $scope->getType($functionCall->getArgs()[0]->value); $iterableAtLeastOnce = $argType->isIterableAtLeastOnce(); if ($iterableAtLeastOnce->no()) { return new ConstantBooleanType(false); diff --git a/src/Type/Php/ArrayPopFunctionReturnTypeExtension.php b/src/Type/Php/ArrayPopFunctionReturnTypeExtension.php index 2e26f43a2b..75c42f8b7b 100644 --- a/src/Type/Php/ArrayPopFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayPopFunctionReturnTypeExtension.php @@ -6,12 +6,14 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function count; -class ArrayPopFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class ArrayPopFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -21,11 +23,11 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - if (!isset($functionCall->args[0])) { + if (!isset($functionCall->getArgs()[0])) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } - $argType = $scope->getType($functionCall->args[0]->value); + $argType = $scope->getType($functionCall->getArgs()[0]->value); $iterableAtLeastOnce = $argType->isIterableAtLeastOnce(); if ($iterableAtLeastOnce->no()) { return new NullType(); diff --git a/src/Type/Php/ArrayRandFunctionReturnTypeExtension.php b/src/Type/Php/ArrayRandFunctionReturnTypeExtension.php index 00444e498f..86edddb617 100644 --- a/src/Type/Php/ArrayRandFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayRandFunctionReturnTypeExtension.php @@ -8,13 +8,15 @@ use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; +use function count; -class ArrayRandFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class ArrayRandFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -24,12 +26,12 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - $argsCount = count($functionCall->args); - if (count($functionCall->args) < 1) { + $argsCount = count($functionCall->getArgs()); + if ($argsCount < 1) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } - $firstArgType = $scope->getType($functionCall->args[0]->value); + $firstArgType = $scope->getType($functionCall->getArgs()[0]->value); $isInteger = (new IntegerType())->isSuperTypeOf($firstArgType->getIterableKeyType()); $isString = (new StringType())->isSuperTypeOf($firstArgType->getIterableKeyType()); @@ -45,7 +47,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $valueType; } - $secondArgType = $scope->getType($functionCall->args[1]->value); + $secondArgType = $scope->getType($functionCall->getArgs()[1]->value); if ($secondArgType instanceof ConstantIntegerType) { if ($secondArgType->getValue() === 1) { diff --git a/src/Type/Php/ArrayReduceFunctionReturnTypeExtension.php b/src/Type/Php/ArrayReduceFunctionReturnTypeExtension.php index ec876b45a7..775ac8de55 100644 --- a/src/Type/Php/ArrayReduceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayReduceFunctionReturnTypeExtension.php @@ -6,12 +6,14 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function count; -class ArrayReduceFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class ArrayReduceFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -21,28 +23,28 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - if (!isset($functionCall->args[1])) { + if (!isset($functionCall->getArgs()[1])) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } - $callbackType = $scope->getType($functionCall->args[1]->value); + $callbackType = $scope->getType($functionCall->getArgs()[1]->value); if ($callbackType->isCallable()->no()) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } $callbackReturnType = ParametersAcceptorSelector::selectFromArgs( $scope, - $functionCall->args, - $callbackType->getCallableParametersAcceptors($scope) + $functionCall->getArgs(), + $callbackType->getCallableParametersAcceptors($scope), )->getReturnType(); - if (isset($functionCall->args[2])) { - $initialType = $scope->getType($functionCall->args[2]->value); + if (isset($functionCall->getArgs()[2])) { + $initialType = $scope->getType($functionCall->getArgs()[2]->value); } else { $initialType = new NullType(); } - $arraysType = $scope->getType($functionCall->args[0]->value); + $arraysType = $scope->getType($functionCall->getArgs()[0]->value); $constantArrays = TypeUtils::getConstantArrays($arraysType); if (count($constantArrays) > 0) { $onlyEmpty = true; diff --git a/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php b/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php index 4f893d81ca..37421aa683 100644 --- a/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayReverseFunctionReturnTypeExtension.php @@ -19,11 +19,11 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - if (!isset($functionCall->args[0])) { + if (!isset($functionCall->getArgs()[0])) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } - return $scope->getType($functionCall->args[0]->value); + return $scope->getType($functionCall->getArgs()[0]->value); } } diff --git a/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php index 2c56f6c47c..997178b24d 100644 --- a/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php @@ -18,6 +18,7 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; +use function count; final class ArraySearchFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -29,12 +30,12 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - $argsCount = count($functionCall->args); + $argsCount = count($functionCall->getArgs()); if ($argsCount < 2) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } - $haystackArgType = $scope->getType($functionCall->args[1]->value); + $haystackArgType = $scope->getType($functionCall->getArgs()[1]->value); $haystackIsArray = (new ArrayType(new MixedType(), new MixedType()))->isSuperTypeOf($haystackArgType); if ($haystackIsArray->no()) { return new NullType(); @@ -44,14 +45,14 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return TypeCombinator::union($haystackArgType->getIterableKeyType(), new ConstantBooleanType(false)); } - $strictArgType = $scope->getType($functionCall->args[2]->value); + $strictArgType = $scope->getType($functionCall->getArgs()[2]->value); if (!($strictArgType instanceof ConstantBooleanType)) { return TypeCombinator::union($haystackArgType->getIterableKeyType(), new ConstantBooleanType(false), new NullType()); } elseif ($strictArgType->getValue() === false) { return TypeCombinator::union($haystackArgType->getIterableKeyType(), new ConstantBooleanType(false)); } - $needleArgType = $scope->getType($functionCall->args[0]->value); + $needleArgType = $scope->getType($functionCall->getArgs()[0]->value); if ($haystackArgType->getIterableValueType()->isSuperTypeOf($needleArgType)->no()) { return new ConstantBooleanType(false); } @@ -90,7 +91,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return TypeCombinator::union( $iterableKeyType, new ConstantBooleanType(false), - ...$typesFromConstantArrays + ...$typesFromConstantArrays, ); } @@ -134,7 +135,6 @@ private function resolveTypeFromConstantHaystackAndNeedle(Type $needle, Constant } /** - * @param Type $type * @return Type[] */ private function pickArrays(Type $type): array diff --git a/src/Type/Php/ArrayShiftFunctionReturnTypeExtension.php b/src/Type/Php/ArrayShiftFunctionReturnTypeExtension.php index b7feab7eca..b38a2889c5 100644 --- a/src/Type/Php/ArrayShiftFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayShiftFunctionReturnTypeExtension.php @@ -6,12 +6,14 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function count; -class ArrayShiftFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class ArrayShiftFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -21,11 +23,11 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - if (!isset($functionCall->args[0])) { + if (!isset($functionCall->getArgs()[0])) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } - $argType = $scope->getType($functionCall->args[0]->value); + $argType = $scope->getType($functionCall->getArgs()[0]->value); $iterableAtLeastOnce = $argType->isIterableAtLeastOnce(); if ($iterableAtLeastOnce->no()) { return new NullType(); diff --git a/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php b/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php index ac42cf83f9..14b51256c7 100644 --- a/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php @@ -9,14 +9,17 @@ use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function array_map; +use function count; -class ArraySliceFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class ArraySliceFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -27,22 +30,22 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope + Scope $scope, ): Type { - $arrayArg = $functionCall->args[0]->value ?? null; + $arrayArg = $functionCall->getArgs()[0]->value ?? null; if ($arrayArg === null) { return new ArrayType( new IntegerType(), - new MixedType() + new MixedType(), ); } $valueType = $scope->getType($arrayArg); - if (isset($functionCall->args[1])) { - $offset = $scope->getType($functionCall->args[1]->value); + if (isset($functionCall->getArgs()[1])) { + $offset = $scope->getType($functionCall->getArgs()[1]->value); if (!$offset instanceof ConstantIntegerType) { $offset = new ConstantIntegerType(0); } @@ -50,8 +53,8 @@ public function getTypeFromFunctionCall( $offset = new ConstantIntegerType(0); } - if (isset($functionCall->args[2])) { - $limit = $scope->getType($functionCall->args[2]->value); + if (isset($functionCall->getArgs()[2])) { + $limit = $scope->getType($functionCall->getArgs()[2]->value); if (!$limit instanceof ConstantIntegerType) { $limit = new NullType(); } @@ -67,20 +70,18 @@ public function getTypeFromFunctionCall( } return new ArrayType( new MixedType(), - new MixedType() + new MixedType(), ); } - if (isset($functionCall->args[3])) { - $preserveKeys = $scope->getType($functionCall->args[3]->value); + if (isset($functionCall->getArgs()[3])) { + $preserveKeys = $scope->getType($functionCall->getArgs()[3]->value); $preserveKeys = (new ConstantBooleanType(true))->isSuperTypeOf($preserveKeys)->yes(); } else { $preserveKeys = false; } - $arrayTypes = array_map(static function (ConstantArrayType $constantArray) use ($offset, $limit, $preserveKeys): ConstantArrayType { - return $constantArray->slice($offset->getValue(), $limit->getValue(), $preserveKeys); - }, $constantArrays); + $arrayTypes = array_map(static fn (ConstantArrayType $constantArray): ConstantArrayType => $constantArray->slice($offset->getValue(), $limit->getValue(), $preserveKeys), $constantArrays); return TypeCombinator::union(...$arrayTypes); } diff --git a/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php b/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..27e763c0d9 --- /dev/null +++ b/src/Type/Php/ArraySpliceFunctionReturnTypeExtension.php @@ -0,0 +1,36 @@ +getName() === 'array_splice'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): Type + { + if (!isset($functionCall->getArgs()[0])) { + return ParametersAcceptorSelector::selectFromArgs($scope, $functionCall->getArgs(), $functionReflection->getVariants())->getReturnType(); + } + + $arrayArg = $scope->getType($functionCall->getArgs()[0]->value); + + return new ArrayType($arrayArg->getIterableKeyType(), $arrayArg->getIterableValueType()); + } + +} diff --git a/src/Type/Php/ArraySumFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArraySumFunctionDynamicReturnTypeExtension.php index aa3cca722f..03a5d15910 100644 --- a/src/Type/Php/ArraySumFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArraySumFunctionDynamicReturnTypeExtension.php @@ -24,11 +24,11 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - if (!isset($functionCall->args[0])) { + if (!isset($functionCall->getArgs()[0])) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } - $arrayType = $scope->getType($functionCall->args[0]->value); + $arrayType = $scope->getType($functionCall->getArgs()[0]->value); $itemType = $arrayType->getIterableValueType(); if ($arrayType->isIterableAtLeastOnce()->no()) { diff --git a/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php index 0c43caa801..8bd638a5b7 100644 --- a/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayValuesFunctionDynamicReturnTypeExtension.php @@ -7,13 +7,14 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; -class ArrayValuesFunctionDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class ArrayValuesFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -23,7 +24,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - $arrayArg = $functionCall->args[0]->value ?? null; + $arrayArg = $functionCall->getArgs()[0]->value ?? null; if ($arrayArg !== null) { $valueType = $scope->getType($arrayArg); if ($valueType->isArray()->yes()) { @@ -36,7 +37,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new ArrayType( new IntegerType(), - new MixedType() + new MixedType(), ); } diff --git a/src/Type/Php/AssertFunctionTypeSpecifyingExtension.php b/src/Type/Php/AssertFunctionTypeSpecifyingExtension.php index c6412e9339..413a163535 100644 --- a/src/Type/Php/AssertFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/AssertFunctionTypeSpecifyingExtension.php @@ -14,17 +14,17 @@ class AssertFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { return $functionReflection->getName() === 'assert' - && isset($node->args[0]); + && isset($node->getArgs()[0]); } public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - return $this->typeSpecifier->specifyTypesInCondition($scope, $node->args[0]->value, TypeSpecifierContext::createTruthy()); + return $this->typeSpecifier->specifyTypesInCondition($scope, $node->getArgs()[0]->value, TypeSpecifierContext::createTruthy()); } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/src/Type/Php/Base64DecodeDynamicFunctionReturnTypeExtension.php b/src/Type/Php/Base64DecodeDynamicFunctionReturnTypeExtension.php index dc36db80d4..158f1da5b1 100644 --- a/src/Type/Php/Base64DecodeDynamicFunctionReturnTypeExtension.php +++ b/src/Type/Php/Base64DecodeDynamicFunctionReturnTypeExtension.php @@ -7,12 +7,13 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\MixedType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; -class Base64DecodeDynamicFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class Base64DecodeDynamicFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -23,14 +24,14 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope + Scope $scope, ): Type { - if (!isset($functionCall->args[1])) { + if (!isset($functionCall->getArgs()[1])) { return new StringType(); } - $argType = $scope->getType($functionCall->args[1]->value); + $argType = $scope->getType($functionCall->getArgs()[1]->value); if ($argType instanceof MixedType) { return new BenevolentUnionType([new StringType(), new ConstantBooleanType(false)]); diff --git a/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php b/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php index a3bc589d4f..0d95c792fd 100644 --- a/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php +++ b/src/Type/Php/BcMathStringOrNullReturnTypeExtension.php @@ -9,6 +9,7 @@ use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\ConstantScalarType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\NullType; @@ -19,7 +20,7 @@ use function in_array; use function is_numeric; -class BcMathStringOrNullReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class BcMathStringOrNullReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -41,18 +42,18 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $defaultReturnType = new UnionType([$stringAndNumericStringType, new NullType()]); - if (isset($functionCall->args[1]) === false) { + if (isset($functionCall->getArgs()[1]) === false) { return $stringAndNumericStringType; } - $secondArgument = $scope->getType($functionCall->args[1]->value); + $secondArgument = $scope->getType($functionCall->getArgs()[1]->value); $secondArgumentIsNumeric = ($secondArgument instanceof ConstantScalarType && is_numeric($secondArgument->getValue())) || $secondArgument instanceof IntegerType; if ($secondArgument instanceof ConstantScalarType && ($this->isZero($secondArgument->getValue()) || !$secondArgumentIsNumeric)) { return new NullType(); } - if (isset($functionCall->args[2]) === false) { + if (isset($functionCall->getArgs()[2]) === false) { if ($secondArgument instanceof ConstantScalarType || $secondArgumentIsNumeric) { return $stringAndNumericStringType; } @@ -60,7 +61,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $defaultReturnType; } - $thirdArgument = $scope->getType($functionCall->args[2]->value); + $thirdArgument = $scope->getType($functionCall->getArgs()[2]->value); $thirdArgumentIsNumeric = ($thirdArgument instanceof ConstantScalarType && is_numeric($thirdArgument->getValue())) || $thirdArgument instanceof IntegerType; if ($thirdArgument instanceof ConstantScalarType && !is_numeric($thirdArgument->getValue())) { @@ -79,20 +80,17 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, * https://www.php.net/manual/en/function.bcsqrt.php * > Returns the square root as a string, or NULL if operand is negative. * - * @param FuncCall $functionCall - * @param Scope $scope - * @return Type */ private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type { $stringAndNumericStringType = TypeCombinator::intersect(new StringType(), new AccessoryNumericStringType()); $defaultReturnType = new UnionType([$stringAndNumericStringType, new NullType()]); - if (isset($functionCall->args[0]) === false) { + if (isset($functionCall->getArgs()[0]) === false) { return $defaultReturnType; } - $firstArgument = $scope->getType($functionCall->args[0]->value); + $firstArgument = $scope->getType($functionCall->getArgs()[0]->value); $firstArgumentIsPositive = $firstArgument instanceof ConstantScalarType && is_numeric($firstArgument->getValue()) && $firstArgument->getValue() >= 0; $firstArgumentIsNegative = $firstArgument instanceof ConstantScalarType && is_numeric($firstArgument->getValue()) && $firstArgument->getValue() < 0; @@ -102,7 +100,7 @@ private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type return new NullType(); } - if (isset($functionCall->args[1]) === false) { + if (isset($functionCall->getArgs()[1]) === false) { if ($firstArgumentIsPositive) { return $stringAndNumericStringType; } @@ -110,7 +108,7 @@ private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type return $defaultReturnType; } - $secondArgument = $scope->getType($functionCall->args[1]->value); + $secondArgument = $scope->getType($functionCall->getArgs()[1]->value); $secondArgumentIsValid = $secondArgument instanceof ConstantScalarType && is_numeric($secondArgument->getValue()) && !$this->isZero($secondArgument->getValue()); $secondArgumentIsNonNumeric = $secondArgument instanceof ConstantScalarType && !is_numeric($secondArgument->getValue()); @@ -129,19 +127,16 @@ private function getTypeForBcSqrt(FuncCall $functionCall, Scope $scope): Type * bcpowmod() * https://www.php.net/manual/en/function.bcpowmod.php * > Returns the result as a string, or FALSE if modulus is 0 or exponent is negative. - * @param FuncCall $functionCall - * @param Scope $scope - * @return Type */ private function getTypeForBcPowMod(FuncCall $functionCall, Scope $scope): Type { $stringAndNumericStringType = TypeCombinator::intersect(new StringType(), new AccessoryNumericStringType()); - if (isset($functionCall->args[1]) === false) { + if (isset($functionCall->getArgs()[1]) === false) { return new UnionType([$stringAndNumericStringType, new ConstantBooleanType(false)]); } - $exponent = $scope->getType($functionCall->args[1]->value); + $exponent = $scope->getType($functionCall->getArgs()[1]->value); $exponentIsNegative = IntegerRangeType::fromInterval(null, 0)->isSuperTypeOf($exponent)->yes(); if ($exponent instanceof ConstantScalarType) { @@ -152,8 +147,8 @@ private function getTypeForBcPowMod(FuncCall $functionCall, Scope $scope): Type return new ConstantBooleanType(false); } - if (isset($functionCall->args[2])) { - $modulus = $scope->getType($functionCall->args[2]->value); + if (isset($functionCall->getArgs()[2])) { + $modulus = $scope->getType($functionCall->getArgs()[2]->value); $modulusIsZero = $modulus instanceof ConstantScalarType && $this->isZero($modulus->getValue()); $modulusIsNonNumeric = $modulus instanceof ConstantScalarType && !is_numeric($modulus->getValue()); @@ -173,7 +168,6 @@ private function getTypeForBcPowMod(FuncCall $functionCall, Scope $scope): Type * Utility to help us determine if value is zero. Handles cases where we pass "0.000" too. * * @param mixed $value - * @return bool */ private function isZero($value): bool { diff --git a/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php index 1c78cb457b..7d9436c55f 100644 --- a/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/ClassExistsFunctionTypeSpecifyingExtension.php @@ -16,8 +16,8 @@ use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FunctionTypeSpecifyingExtension; -use PHPStan\Type\NeverType; -use PHPStan\Type\TypeCombinator; +use function in_array; +use function ltrim; class ClassExistsFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { @@ -27,42 +27,38 @@ class ClassExistsFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyi public function isFunctionSupported( FunctionReflection $functionReflection, FuncCall $node, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): bool { return in_array($functionReflection->getName(), [ 'class_exists', 'interface_exists', 'trait_exists', - ], true) && isset($node->args[0]) && $context->truthy(); + ], true) && isset($node->getArgs()[0]) && $context->truthy(); } public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - $argType = $scope->getType($node->args[0]->value); + $argType = $scope->getType($node->getArgs()[0]->value); $classStringType = new ClassStringType(); - if (TypeCombinator::intersect($argType, $classStringType) instanceof NeverType) { - if ($argType instanceof ConstantStringType) { - return $this->typeSpecifier->create( - new FuncCall(new FullyQualified('class_exists'), [ - new Arg(new String_(ltrim($argType->getValue(), '\\'))), - ]), - new ConstantBooleanType(true), - $context, - false, - $scope - ); - } - - return new SpecifiedTypes(); + if ($argType instanceof ConstantStringType) { + return $this->typeSpecifier->create( + new FuncCall(new FullyQualified('class_exists'), [ + new Arg(new String_(ltrim($argType->getValue(), '\\'))), + ]), + new ConstantBooleanType(true), + $context, + false, + $scope, + ); } return $this->typeSpecifier->create( - $node->args[0]->value, + $node->getArgs()[0]->value, $classStringType, $context, false, - $scope + $scope, ); } diff --git a/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php b/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php index 9f64a13b7a..312a3cd42c 100644 --- a/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureBindDynamicReturnTypeExtension.php @@ -2,19 +2,21 @@ namespace PHPStan\Type\Php; +use Closure; 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; -class ClosureBindDynamicReturnTypeExtension implements \PHPStan\Type\DynamicStaticMethodReturnTypeExtension +class ClosureBindDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { public function getClass(): string { - return \Closure::class; + return Closure::class; } public function isStaticMethodSupported(MethodReflection $methodReflection): bool @@ -24,7 +26,7 @@ public function isStaticMethodSupported(MethodReflection $methodReflection): boo public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type { - $closureType = $scope->getType($methodCall->args[0]->value); + $closureType = $scope->getType($methodCall->getArgs()[0]->value); if (!($closureType instanceof ClosureType)) { return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); } diff --git a/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php b/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php index 1ea706a5ec..d2d74ff813 100644 --- a/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureBindToDynamicReturnTypeExtension.php @@ -2,19 +2,21 @@ namespace PHPStan\Type\Php; +use Closure; 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; -class ClosureBindToDynamicReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension +class ClosureBindToDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { public function getClass(): string { - return \Closure::class; + return Closure::class; } public function isMethodSupported(MethodReflection $methodReflection): bool diff --git a/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php b/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php index 8f3cf04574..f5f8de9f1b 100644 --- a/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php +++ b/src/Type/Php/ClosureFromCallableDynamicReturnTypeExtension.php @@ -2,20 +2,23 @@ namespace PHPStan\Type\Php; +use Closure; 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\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class ClosureFromCallableDynamicReturnTypeExtension implements \PHPStan\Type\DynamicStaticMethodReturnTypeExtension +class ClosureFromCallableDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { public function getClass(): string { - return \Closure::class; + return Closure::class; } public function isStaticMethodSupported(MethodReflection $methodReflection): bool @@ -25,7 +28,15 @@ public function isStaticMethodSupported(MethodReflection $methodReflection): boo public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type { - $callableType = $scope->getType($methodCall->args[0]->value); + if (!isset($methodCall->getArgs()[0])) { + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); + } + + $callableType = $scope->getType($methodCall->getArgs()[0]->value); if ($callableType->isCallable()->no()) { return new ErrorType(); } @@ -36,7 +47,9 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, $closureTypes[] = new ClosureType( $parameters, $variant->getReturnType(), - $variant->isVariadic() + $variant->isVariadic(), + $variant->getTemplateTypeMap(), + $variant->getResolvedTemplateTypeMap(), ); } diff --git a/src/Type/Php/CompactFunctionReturnTypeExtension.php b/src/Type/Php/CompactFunctionReturnTypeExtension.php index ccd1cb8e47..e17ddfb6a5 100644 --- a/src/Type/Php/CompactFunctionReturnTypeExtension.php +++ b/src/Type/Php/CompactFunctionReturnTypeExtension.php @@ -11,15 +11,14 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; +use function array_merge; +use function count; class CompactFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - private bool $checkMaybeUndefinedVariables; - - public function __construct(bool $checkMaybeUndefinedVariables) + public function __construct(private bool $checkMaybeUndefinedVariables) { - $this->checkMaybeUndefinedVariables = $checkMaybeUndefinedVariables; } public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -30,11 +29,11 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope + Scope $scope, ): Type { $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - if (count($functionCall->args) === 0) { + if (count($functionCall->getArgs()) === 0) { return $defaultReturnType; } @@ -43,7 +42,7 @@ public function getTypeFromFunctionCall( } $array = ConstantArrayTypeBuilder::createEmpty(); - foreach ($functionCall->args as $arg) { + foreach ($functionCall->getArgs() as $arg) { $type = $scope->getType($arg->value); $constantStrings = $this->findConstantStrings($type); if ($constantStrings === null) { @@ -63,7 +62,6 @@ public function getTypeFromFunctionCall( } /** - * @param Type $type * @return array|null */ private function findConstantStrings(Type $type): ?array diff --git a/src/Type/Php/CountFunctionReturnTypeExtension.php b/src/Type/Php/CountFunctionReturnTypeExtension.php index 088717c349..c9d13e34cd 100644 --- a/src/Type/Php/CountFunctionReturnTypeExtension.php +++ b/src/Type/Php/CountFunctionReturnTypeExtension.php @@ -7,38 +7,42 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function count; +use function in_array; +use const COUNT_RECURSIVE; -class CountFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class CountFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return $functionReflection->getName() === 'count'; + return in_array($functionReflection->getName(), ['sizeof', 'count'], true); } public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope + Scope $scope, ): Type { - if (count($functionCall->args) < 1) { + if (count($functionCall->getArgs()) < 1) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } - if (count($functionCall->args) > 1) { - $mode = $scope->getType($functionCall->args[1]->value); - if ($mode->isSuperTypeOf(new ConstantIntegerType(\COUNT_RECURSIVE))->yes()) { + if (count($functionCall->getArgs()) > 1) { + $mode = $scope->getType($functionCall->getArgs()[1]->value); + if ($mode->isSuperTypeOf(new ConstantIntegerType(COUNT_RECURSIVE))->yes()) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } } - $argType = $scope->getType($functionCall->args[0]->value); - $constantArrays = TypeUtils::getConstantArrays($scope->getType($functionCall->args[0]->value)); + $argType = $scope->getType($functionCall->getArgs()[0]->value); + $constantArrays = TypeUtils::getConstantArrays($scope->getType($functionCall->getArgs()[0]->value)); if (count($constantArrays) === 0) { if ($argType->isIterableAtLeastOnce()->yes()) { return IntegerRangeType::fromInterval(1, null); diff --git a/src/Type/Php/CountFunctionTypeSpecifyingExtension.php b/src/Type/Php/CountFunctionTypeSpecifyingExtension.php index 7fdde34e13..8fd8f91f84 100644 --- a/src/Type/Php/CountFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/CountFunctionTypeSpecifyingExtension.php @@ -13,35 +13,37 @@ use PHPStan\Type\ArrayType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\MixedType; +use function count; +use function in_array; class CountFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function isFunctionSupported( FunctionReflection $functionReflection, FuncCall $node, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): bool { return !$context->null() - && count($node->args) >= 1 - && $functionReflection->getName() === 'count'; + && count($node->getArgs()) >= 1 + && in_array($functionReflection->getName(), ['sizeof', 'count'], true); } public function specifyTypes( FunctionReflection $functionReflection, FuncCall $node, Scope $scope, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): SpecifiedTypes { - if (!(new ArrayType(new MixedType(), new MixedType()))->isSuperTypeOf($scope->getType($node->args[0]->value))->yes()) { + if (!(new ArrayType(new MixedType(), new MixedType()))->isSuperTypeOf($scope->getType($node->getArgs()[0]->value))->yes()) { return new SpecifiedTypes([], []); } - return $this->typeSpecifier->create($node->args[0]->value, new NonEmptyArrayType(), $context, false, $scope); + return $this->typeSpecifier->create($node->getArgs()[0]->value, new NonEmptyArrayType(), $context, false, $scope); } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/src/Type/Php/CurlInitReturnTypeExtension.php b/src/Type/Php/CurlInitReturnTypeExtension.php index 6777c14666..e9564278fe 100644 --- a/src/Type/Php/CurlInitReturnTypeExtension.php +++ b/src/Type/Php/CurlInitReturnTypeExtension.php @@ -2,14 +2,17 @@ namespace PHPStan\Type\Php; +use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function count; -class CurlInitReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class CurlInitReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -19,11 +22,11 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, - \PhpParser\Node\Expr\FuncCall $functionCall, - Scope $scope + Node\Expr\FuncCall $functionCall, + Scope $scope, ): Type { - $argsCount = count($functionCall->args); + $argsCount = count($functionCall->getArgs()); $returnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); if ($argsCount === 0) { return TypeCombinator::remove($returnType, new ConstantBooleanType(false)); diff --git a/src/Type/Php/StrvalFunctionReturnTypeExtension.php b/src/Type/Php/DateFormatFunctionReturnTypeExtension.php similarity index 51% rename from src/Type/Php/StrvalFunctionReturnTypeExtension.php rename to src/Type/Php/DateFormatFunctionReturnTypeExtension.php index 391003efcb..b926ed181d 100644 --- a/src/Type/Php/StrvalFunctionReturnTypeExtension.php +++ b/src/Type/Php/DateFormatFunctionReturnTypeExtension.php @@ -3,31 +3,37 @@ 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; -use PHPStan\Type\NullType; +use PHPStan\Type\StringType; use PHPStan\Type\Type; +use function count; -class StrvalFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension +class DateFormatFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return $functionReflection->getName() === 'strval'; + return $functionReflection->getName() === 'date_format'; } public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope + Scope $scope, ): Type { - if (count($functionCall->args) === 0) { - return new NullType(); + if (count($functionCall->getArgs()) < 2) { + return new StringType(); } - $argType = $scope->getType($functionCall->args[0]->value); - return $argType->toString(); + + return $scope->getType( + new FuncCall(new FullyQualified('date'), [ + $functionCall->getArgs()[1], + ]), + ); } } diff --git a/src/Type/Php/DateFormatMethodReturnTypeExtension.php b/src/Type/Php/DateFormatMethodReturnTypeExtension.php new file mode 100644 index 0000000000..f6b71786bc --- /dev/null +++ b/src/Type/Php/DateFormatMethodReturnTypeExtension.php @@ -0,0 +1,42 @@ +getName() === 'format'; + } + + 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], + ]), + ); + } + +} diff --git a/src/Type/Php/DateFunctionReturnTypeExtension.php b/src/Type/Php/DateFunctionReturnTypeExtension.php index 7d4792c03f..ce9b739295 100644 --- a/src/Type/Php/DateFunctionReturnTypeExtension.php +++ b/src/Type/Php/DateFunctionReturnTypeExtension.php @@ -5,12 +5,20 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\ConstantTypeHelper; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use PHPStan\Type\UnionType; +use function count; +use function date; +use function sprintf; class DateFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -23,29 +31,95 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope + Scope $scope, ): Type { - if (count($functionCall->args) === 0) { + if (count($functionCall->getArgs()) === 0) { return new StringType(); } - $argType = $scope->getType($functionCall->args[0]->value); + $argType = $scope->getType($functionCall->getArgs()[0]->value); $constantStrings = TypeUtils::getConstantStrings($argType); + if (count($constantStrings) === 0) { return new StringType(); } + if (count($constantStrings) === 1) { + $constantString = $constantStrings[0]->getValue(); + + // see see https://www.php.net/manual/en/datetime.format.php + switch ($constantString) { + case 'd': + return $this->buildNumericRangeType(1, 31, true); + case 'j': + return $this->buildNumericRangeType(1, 31, false); + case 'N': + return $this->buildNumericRangeType(1, 7, false); + case 'w': + return $this->buildNumericRangeType(0, 6, false); + case 'm': + return $this->buildNumericRangeType(1, 12, true); + case 'n': + return $this->buildNumericRangeType(1, 12, false); + case 't': + return $this->buildNumericRangeType(28, 31, false); + case 'L': + return $this->buildNumericRangeType(0, 1, false); + case 'g': + return $this->buildNumericRangeType(1, 12, false); + case 'G': + return $this->buildNumericRangeType(0, 23, false); + case 'h': + return $this->buildNumericRangeType(1, 12, true); + case 'H': + return $this->buildNumericRangeType(0, 23, true); + case 'I': + return $this->buildNumericRangeType(0, 1, false); + } + } + + $types = []; foreach ($constantStrings as $constantString) { - $formattedDate = date($constantString->getValue()); - if (!is_numeric($formattedDate)) { - return new StringType(); + $types[] = ConstantTypeHelper::getTypeFromValue(date($constantString->getValue())); + } + + $type = TypeCombinator::union(...$types); + if ($type->isNumericString()->yes()) { + return new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ]); + } + + 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 new IntersectionType([ - new StringType(), - new AccessoryNumericStringType(), - ]); + return new UnionType($types); } } diff --git a/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php b/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php index 047d196f61..e7fda0fa2c 100644 --- a/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php +++ b/src/Type/Php/DateIntervalConstructorThrowTypeExtension.php @@ -11,6 +11,7 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function count; class DateIntervalConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { @@ -22,16 +23,16 @@ public function isStaticMethodSupported(MethodReflection $methodReflection): boo public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type { - if (count($methodCall->args) === 0) { + if (count($methodCall->getArgs()) === 0) { return $methodReflection->getThrowType(); } - $valueType = $scope->getType($methodCall->args[0]->value); + $valueType = $scope->getType($methodCall->getArgs()[0]->value); $constantStrings = TypeUtils::getConstantStrings($valueType); foreach ($constantStrings as $constantString) { try { - new \DateInterval($constantString->getValue()); + new DateInterval($constantString->getValue()); } catch (\Exception $e) { // phpcs:ignore return $methodReflection->getThrowType(); } diff --git a/src/Type/Php/DatePeriodConstructorReturnTypeExtension.php b/src/Type/Php/DatePeriodConstructorReturnTypeExtension.php new file mode 100644 index 0000000000..8b52604f48 --- /dev/null +++ b/src/Type/Php/DatePeriodConstructorReturnTypeExtension.php @@ -0,0 +1,85 @@ +getName() === '__construct'; + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type + { + if (!isset($methodCall->getArgs()[0])) { + return new ObjectType(DatePeriod::class); + } + + if (!$methodCall->class instanceof Name) { + return new ObjectType(DatePeriod::class); + } + + $className = $scope->resolveName($methodCall->class); + if (strtolower($className) !== 'dateperiod') { + return new ObjectType($className); + } + + $firstArgType = $scope->getType($methodCall->getArgs()[0]->value); + if ((new StringType())->isSuperTypeOf($firstArgType)->yes()) { + $firstArgType = new ObjectType(DateTime::class); + } + $thirdArgType = null; + if (isset($methodCall->getArgs()[2])) { + $thirdArgType = $scope->getType($methodCall->getArgs()[2]->value); + } + + if (!$thirdArgType instanceof Type) { + return new GenericObjectType(DatePeriod::class, [ + $firstArgType, + new NullType(), + new IntegerType(), + ]); + } + + if ((new ObjectType(DateTimeInterface::class))->isSuperTypeOf($thirdArgType)->yes()) { + return new GenericObjectType(DatePeriod::class, [ + $firstArgType, + $thirdArgType, + new NullType(), + ]); + } + + if ((new IntegerType())->isSuperTypeOf($thirdArgType)->yes()) { + return new GenericObjectType(DatePeriod::class, [ + $firstArgType, + new NullType(), + $thirdArgType, + ]); + } + + return new ObjectType(DatePeriod::class); + } + +} diff --git a/src/Type/Php/DateTimeConstructorThrowTypeExtension.php b/src/Type/Php/DateTimeConstructorThrowTypeExtension.php index 102996ff9d..c84512c05a 100644 --- a/src/Type/Php/DateTimeConstructorThrowTypeExtension.php +++ b/src/Type/Php/DateTimeConstructorThrowTypeExtension.php @@ -12,6 +12,8 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function count; +use function in_array; class DateTimeConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { @@ -23,16 +25,16 @@ public function isStaticMethodSupported(MethodReflection $methodReflection): boo public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type { - if (count($methodCall->args) === 0) { + if (count($methodCall->getArgs()) === 0) { return null; } - $valueType = $scope->getType($methodCall->args[0]->value); + $valueType = $scope->getType($methodCall->getArgs()[0]->value); $constantStrings = TypeUtils::getConstantStrings($valueType); foreach ($constantStrings as $constantString) { try { - new \DateTime($constantString->getValue()); + new DateTime($constantString->getValue()); } catch (\Exception $e) { // phpcs:ignore return $methodReflection->getThrowType(); } diff --git a/src/Type/Php/DateTimeDynamicReturnTypeExtension.php b/src/Type/Php/DateTimeDynamicReturnTypeExtension.php index 48e3820856..22945f9966 100644 --- a/src/Type/Php/DateTimeDynamicReturnTypeExtension.php +++ b/src/Type/Php/DateTimeDynamicReturnTypeExtension.php @@ -13,6 +13,8 @@ use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use function count; +use function in_array; class DateTimeDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -26,12 +28,12 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, { $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - if (count($functionCall->args) < 2) { + if (count($functionCall->getArgs()) < 2) { return $defaultReturnType; } - $format = $scope->getType($functionCall->args[0]->value); - $datetime = $scope->getType($functionCall->args[1]->value); + $format = $scope->getType($functionCall->getArgs()[0]->value); + $datetime = $scope->getType($functionCall->getArgs()[1]->value); if (!$format instanceof ConstantStringType || !$datetime instanceof ConstantStringType) { return $defaultReturnType; diff --git a/src/Type/Php/DefineConstantTypeSpecifyingExtension.php b/src/Type/Php/DefineConstantTypeSpecifyingExtension.php index 0ac5590dca..fa6f5bba57 100644 --- a/src/Type/Php/DefineConstantTypeSpecifyingExtension.php +++ b/src/Type/Php/DefineConstantTypeSpecifyingExtension.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Php; +use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; @@ -11,6 +12,7 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FunctionTypeSpecifyingExtension; +use function count; class DefineConstantTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { @@ -25,22 +27,22 @@ public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void public function isFunctionSupported( FunctionReflection $functionReflection, FuncCall $node, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): bool { return $functionReflection->getName() === 'define' && $context->null() - && count($node->args) >= 2; + && count($node->getArgs()) >= 2; } public function specifyTypes( FunctionReflection $functionReflection, FuncCall $node, Scope $scope, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): SpecifiedTypes { - $constantName = $scope->getType($node->args[0]->value); + $constantName = $scope->getType($node->getArgs()[0]->value); if ( !$constantName instanceof ConstantStringType || $constantName->getValue() === '' @@ -49,13 +51,13 @@ public function specifyTypes( } return $this->typeSpecifier->create( - new \PhpParser\Node\Expr\ConstFetch( - new \PhpParser\Node\Name\FullyQualified($constantName->getValue()) + new Node\Expr\ConstFetch( + new Node\Name\FullyQualified($constantName->getValue()), ), - $scope->getType($node->args[1]->value), + $scope->getType($node->getArgs()[1]->value), TypeSpecifierContext::createTruthy(), false, - $scope + $scope, ); } diff --git a/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php b/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php index 9ce72c1e1b..0e90075013 100644 --- a/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php +++ b/src/Type/Php/DefinedConstantTypeSpecifyingExtension.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Php; +use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; @@ -12,6 +13,7 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\MixedType; +use function count; class DefinedConstantTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { @@ -26,11 +28,11 @@ public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void public function isFunctionSupported( FunctionReflection $functionReflection, FuncCall $node, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): bool { return $functionReflection->getName() === 'defined' - && count($node->args) >= 1 + && count($node->getArgs()) >= 1 && !$context->null(); } @@ -38,10 +40,10 @@ public function specifyTypes( FunctionReflection $functionReflection, FuncCall $node, Scope $scope, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): SpecifiedTypes { - $constantName = $scope->getType($node->args[0]->value); + $constantName = $scope->getType($node->getArgs()[0]->value); if ( !$constantName instanceof ConstantStringType || $constantName->getValue() === '' @@ -50,13 +52,13 @@ public function specifyTypes( } return $this->typeSpecifier->create( - new \PhpParser\Node\Expr\ConstFetch( - new \PhpParser\Node\Name\FullyQualified($constantName->getValue()) + new Node\Expr\ConstFetch( + new Node\Name\FullyQualified($constantName->getValue()), ), new MixedType(), $context, false, - $scope + $scope, ); } diff --git a/src/Type/Php/DioStatDynamicFunctionReturnTypeExtension.php b/src/Type/Php/DioStatDynamicFunctionReturnTypeExtension.php index a134c6d4a2..0a96b8d73f 100644 --- a/src/Type/Php/DioStatDynamicFunctionReturnTypeExtension.php +++ b/src/Type/Php/DioStatDynamicFunctionReturnTypeExtension.php @@ -7,11 +7,12 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -class DioStatDynamicFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class DioStatDynamicFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool diff --git a/src/Type/Php/DsMapDynamicReturnTypeExtension.php b/src/Type/Php/DsMapDynamicReturnTypeExtension.php index faf1f5bbeb..140e41a3f5 100644 --- a/src/Type/Php/DsMapDynamicReturnTypeExtension.php +++ b/src/Type/Php/DsMapDynamicReturnTypeExtension.php @@ -12,6 +12,9 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; +use function array_filter; +use function array_values; +use function count; final class DsMapDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { @@ -30,11 +33,11 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method { $returnType = ParametersAcceptorSelector::selectFromArgs( $scope, - $methodCall->args, - $methodReflection->getVariants() + $methodCall->getArgs(), + $methodReflection->getVariants(), )->getReturnType(); - if (count($methodCall->args) > 1) { + if (count($methodCall->getArgs()) > 1) { return $returnType; } @@ -55,8 +58,8 @@ static function (Type $type): bool { } return true; - } - ) + }, + ), ); if (count($types) === 1) { diff --git a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php index 723ab81877..8440f8f2dd 100644 --- a/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ExplodeFunctionDynamicReturnTypeExtension.php @@ -11,6 +11,7 @@ use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; @@ -19,15 +20,13 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function count; -class ExplodeFunctionDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class ExplodeFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - private PhpVersion $phpVersion; - - public function __construct(PhpVersion $phpVersion) + public function __construct(private PhpVersion $phpVersion) { - $this->phpVersion = $phpVersion; } public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -38,14 +37,14 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope + Scope $scope, ): Type { - if (count($functionCall->args) < 2) { + if (count($functionCall->getArgs()) < 2) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } - $delimiterType = $scope->getType($functionCall->args[0]->value); + $delimiterType = $scope->getType($functionCall->getArgs()[0]->value); $isSuperset = (new ConstantStringType(''))->isSuperTypeOf($delimiterType); if ($isSuperset->yes()) { if ($this->phpVersion->getVersionId() >= 80000) { @@ -55,8 +54,8 @@ public function getTypeFromFunctionCall( } elseif ($isSuperset->no()) { $arrayType = new ArrayType(new IntegerType(), new StringType()); if ( - !isset($functionCall->args[2]) - || IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($scope->getType($functionCall->args[2]->value))->yes() + !isset($functionCall->getArgs()[2]) + || IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($scope->getType($functionCall->getArgs()[2]->value))->yes() ) { return TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); } diff --git a/src/Type/Php/FilterVarDynamicReturnTypeExtension.php b/src/Type/Php/FilterVarDynamicReturnTypeExtension.php index c88871a7fd..6340124fef 100644 --- a/src/Type/Php/FilterVarDynamicReturnTypeExtension.php +++ b/src/Type/Php/FilterVarDynamicReturnTypeExtension.php @@ -7,6 +7,8 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\ShouldNotHappenException; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantArrayType; @@ -17,26 +19,30 @@ use PHPStan\Type\ErrorType; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NullType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; +use function sprintf; +use function strtolower; class FilterVarDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - private ReflectionProvider $reflectionProvider; + /** + * All validation filters match 0x100. + */ + private const VALIDATION_FILTER_BITMASK = 0x100; private ConstantStringType $flagsString; /** @var array|null */ private ?array $filterTypeMap = null; - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct(private ReflectionProvider $reflectionProvider) { - $this->reflectionProvider = $reflectionProvider; - $this->flagsString = new ConstantStringType('flags'); } @@ -64,6 +70,7 @@ private function getFilterTypeMap(): array $this->getConstant('FILTER_SANITIZE_STRING') => $stringType, $this->getConstant('FILTER_SANITIZE_URL') => $stringType, $this->getConstant('FILTER_VALIDATE_BOOLEAN') => $booleanType, + $this->getConstant('FILTER_VALIDATE_DOMAIN') => $stringType, $this->getConstant('FILTER_VALIDATE_EMAIL') => $stringType, $this->getConstant('FILTER_VALIDATE_FLOAT') => $floatType, $this->getConstant('FILTER_VALIDATE_INT') => $intType, @@ -89,7 +96,7 @@ private function getConstant(string $constantName): int $constant = $this->reflectionProvider->getConstant(new Node\Name($constantName), null); $valueType = $constant->getValueType(); if (!$valueType instanceof ConstantIntegerType) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Constant %s does not have integer type.', $constantName)); + throw new ShouldNotHappenException(sprintf('Constant %s does not have integer type.', $constantName)); } return $valueType->getValue(); @@ -103,12 +110,12 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope + Scope $scope, ): Type { $mixedType = new MixedType(); - $filterArg = $functionCall->args[1] ?? null; + $filterArg = $functionCall->getArgs()[1] ?? null; if ($filterArg === null) { $filterValue = $this->getConstant('FILTER_DEFAULT'); } else { @@ -119,8 +126,8 @@ public function getTypeFromFunctionCall( $filterValue = $filterType->getValue(); } - $flagsArg = $functionCall->args[2] ?? null; - $inputType = $scope->getType($functionCall->args[0]->value); + $flagsArg = $functionCall->getArgs()[2] ?? null; + $inputType = $scope->getType($functionCall->getArgs()[0]->value); $exactType = $this->determineExactType($inputType, $filterValue); if ($exactType !== null) { $type = $exactType; @@ -128,6 +135,12 @@ public function getTypeFromFunctionCall( $type = $this->getFilterTypeMap()[$filterValue] ?? $mixedType; $otherType = $this->getOtherType($flagsArg, $scope); + if ($inputType->isNonEmptyString()->yes() + && $type instanceof StringType + && !$this->canStringBeSanitized($filterValue, $flagsArg, $scope)) { + $type = new IntersectionType([$type, new AccessoryNonEmptyStringType()]); + } + if ($otherType->isSuperTypeOf($type)->no()) { $type = new UnionType([$type, $otherType]); } @@ -216,4 +229,22 @@ private function getFlagsValue(Type $exprType): Type return $exprType->getOffsetValueType($this->flagsString); } + private function canStringBeSanitized(int $filterValue, ?Node\Arg $flagsArg, Scope $scope): bool + { + // If it is a validation filter, the string will not be changed + if (($filterValue & self::VALIDATION_FILTER_BITMASK) !== 0) { + return false; + } + + // FILTER_DEFAULT will not sanitize, unless it has FILTER_FLAG_STRIP_LOW, + // FILTER_FLAG_STRIP_HIGH, or FILTER_FLAG_STRIP_BACKTICK + if ($filterValue === $this->getConstant('FILTER_DEFAULT')) { + return $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_LOW'), $flagsArg, $scope) + || $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_HIGH'), $flagsArg, $scope) + || $this->hasFlag($this->getConstant('FILTER_FLAG_STRIP_BACKTICK'), $flagsArg, $scope); + } + + return true; + } + } diff --git a/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php b/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php new file mode 100644 index 0000000000..12df64cf17 --- /dev/null +++ b/src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php @@ -0,0 +1,64 @@ +getName() === 'function_exists' && isset($node->getArgs()[0]) && $context->truthy(); + } + + public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + $argType = $scope->getType($node->getArgs()[0]->value); + if ($argType instanceof ConstantStringType) { + return $this->typeSpecifier->create( + new FuncCall(new FullyQualified('function_exists'), [ + new Arg(new String_(ltrim($argType->getValue(), '\\'))), + ]), + new ConstantBooleanType(true), + $context, + false, + $scope, + ); + } + + return $this->typeSpecifier->create( + $node->getArgs()[0]->value, + new CallableType(), + $context, + false, + $scope, + ); + } + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + +} diff --git a/src/Type/Php/GetClassDynamicReturnTypeExtension.php b/src/Type/Php/GetClassDynamicReturnTypeExtension.php index 5a1bc77551..e605618e66 100644 --- a/src/Type/Php/GetClassDynamicReturnTypeExtension.php +++ b/src/Type/Php/GetClassDynamicReturnTypeExtension.php @@ -19,6 +19,7 @@ use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; +use function count; class GetClassDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -30,7 +31,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - $args = $functionCall->args; + $args = $functionCall->getArgs(); if (count($args) === 0) { if ($scope->isInClass()) { return new ConstantStringType($scope->getClassReflection()->getName(), true); @@ -49,9 +50,19 @@ static function (Type $type, callable $traverse): Type { } if ($type instanceof TemplateType && !$type instanceof TypeWithClassName) { - return new GenericClassStringType($type); + if ($type instanceof ObjectWithoutClassType) { + return new GenericClassStringType($type); + } + + return new UnionType([ + new GenericClassStringType($type), + new ConstantBooleanType(false), + ]); } elseif ($type instanceof MixedType) { - return new ClassStringType(); + return new UnionType([ + new ClassStringType(), + new ConstantBooleanType(false), + ]); } elseif ($type instanceof StaticType) { return new GenericClassStringType($type->getStaticObjectType()); } elseif ($type instanceof TypeWithClassName) { @@ -61,7 +72,7 @@ static function (Type $type, callable $traverse): Type { } return new ConstantBooleanType(false); - } + }, ); } diff --git a/src/Type/Php/GetParentClassDynamicFunctionReturnTypeExtension.php b/src/Type/Php/GetParentClassDynamicFunctionReturnTypeExtension.php index b834633536..2ba8086d47 100644 --- a/src/Type/Php/GetParentClassDynamicFunctionReturnTypeExtension.php +++ b/src/Type/Php/GetParentClassDynamicFunctionReturnTypeExtension.php @@ -7,25 +7,27 @@ 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; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; +use function array_map; +use function count; -class GetParentClassDynamicFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class GetParentClassDynamicFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - private \PHPStan\Reflection\ReflectionProvider $reflectionProvider; - - public function __construct(\PHPStan\Reflection\ReflectionProvider $reflectionProvider) + public function __construct(private ReflectionProvider $reflectionProvider) { - $this->reflectionProvider = $reflectionProvider; } public function isFunctionSupported( - FunctionReflection $functionReflection + FunctionReflection $functionReflection, ): bool { return $functionReflection->getName() === 'get_parent_class'; @@ -34,42 +36,38 @@ public function isFunctionSupported( public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope + Scope $scope, ): Type { $defaultReturnType = ParametersAcceptorSelector::selectSingle( - $functionReflection->getVariants() + $functionReflection->getVariants(), )->getReturnType(); - if (count($functionCall->args) === 0) { + if (count($functionCall->getArgs()) === 0) { if ($scope->isInTrait()) { return $defaultReturnType; } if ($scope->isInClass()) { return $this->findParentClassType( - $scope->getClassReflection() + $scope->getClassReflection(), ); } return new ConstantBooleanType(false); } - $argType = $scope->getType($functionCall->args[0]->value); + $argType = $scope->getType($functionCall->getArgs()[0]->value); if ($scope->isInTrait() && TypeUtils::findThisType($argType) !== null) { return $defaultReturnType; } $constantStrings = TypeUtils::getConstantStrings($argType); if (count($constantStrings) > 0) { - return \PHPStan\Type\TypeCombinator::union(...array_map(function (ConstantStringType $stringType): Type { - return $this->findParentClassNameType($stringType->getValue()); - }, $constantStrings)); + return TypeCombinator::union(...array_map(fn (ConstantStringType $stringType): Type => $this->findParentClassNameType($stringType->getValue()), $constantStrings)); } $classNames = TypeUtils::getDirectClassNames($argType); if (count($classNames) > 0) { - return \PHPStan\Type\TypeCombinator::union(...array_map(function (string $classNames): Type { - return $this->findParentClassNameType($classNames); - }, $classNames)); + return TypeCombinator::union(...array_map(fn (string $classNames): Type => $this->findParentClassNameType($classNames), $classNames)); } return $defaultReturnType; @@ -88,11 +86,11 @@ private function findParentClassNameType(string $className): Type } private function findParentClassType( - ClassReflection $classReflection + ClassReflection $classReflection, ): Type { $parentClass = $classReflection->getParentClass(); - if ($parentClass === false) { + if ($parentClass === null) { return new ConstantBooleanType(false); } diff --git a/src/Type/Php/GetoptFunctionDynamicReturnTypeExtension.php b/src/Type/Php/GetoptFunctionDynamicReturnTypeExtension.php deleted file mode 100644 index becf64a90e..0000000000 --- a/src/Type/Php/GetoptFunctionDynamicReturnTypeExtension.php +++ /dev/null @@ -1,26 +0,0 @@ -getName() === 'getopt'; - } - - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type - { - return TypeUtils::toBenevolentUnion(ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType()); - } - -} diff --git a/src/Type/Php/GettimeofdayDynamicFunctionReturnTypeExtension.php b/src/Type/Php/GettimeofdayDynamicFunctionReturnTypeExtension.php index 8fdb910688..92ff653718 100644 --- a/src/Type/Php/GettimeofdayDynamicFunctionReturnTypeExtension.php +++ b/src/Type/Php/GettimeofdayDynamicFunctionReturnTypeExtension.php @@ -9,13 +9,14 @@ use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; -class GettimeofdayDynamicFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class GettimeofdayDynamicFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -38,11 +39,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, ]); $floatType = new FloatType(); - if (!isset($functionCall->args[0])) { + if (!isset($functionCall->getArgs()[0])) { return $arrayType; } - $argType = $scope->getType($functionCall->args[0]->value); + $argType = $scope->getType($functionCall->getArgs()[0]->value); $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($argType); $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($argType); $compareTypes = $isTrueType->compareTo($isFalseType); diff --git a/src/Type/Php/HashFunctionsReturnTypeExtension.php b/src/Type/Php/HashFunctionsReturnTypeExtension.php index 402bc24196..a1c6d2e06e 100644 --- a/src/Type/Php/HashFunctionsReturnTypeExtension.php +++ b/src/Type/Php/HashFunctionsReturnTypeExtension.php @@ -4,43 +4,138 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; +use PHPStan\Type\NeverType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function array_map; +use function hash_algos; +use function in_array; +use function strtolower; final class HashFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + private const SUPPORTED_FUNCTIONS = [ + 'hash' => [ + 'cryptographic' => false, + 'possiblyFalse' => false, + ], + 'hash_file' => [ + 'cryptographic' => false, + 'possiblyFalse' => true, + ], + 'hash_hkdf' => [ + 'cryptographic' => true, + 'possiblyFalse' => false, + ], + 'hash_hmac' => [ + 'cryptographic' => true, + 'possiblyFalse' => false, + ], + 'hash_hmac_file' => [ + 'cryptographic' => true, + 'possiblyFalse' => true, + ], + 'hash_pbkdf2' => [ + 'cryptographic' => true, + 'possiblyFalse' => false, + ], + ]; + + private const NON_CRYPTOGRAPHIC_ALGORITHMS = [ + 'adler32', + 'crc32', + 'crc32b', + 'crc32c', + 'fnv132', + 'fnv1a32', + 'fnv164', + 'fnv1a64', + 'joaat', + 'murmur3a', + 'murmur3c', + 'murmur3f', + 'xxh32', + 'xxh64', + 'xxh3', + 'xxh128', + ]; + + /** @var array */ + private array $hashAlgorithms; + + public function __construct(private PhpVersion $phpVersion) + { + $this->hashAlgorithms = hash_algos(); + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return $functionReflection->getName() === 'hash'; + $name = strtolower($functionReflection->getName()); + return isset(self::SUPPORTED_FUNCTIONS[$name]); } public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - if (!isset($functionCall->args[0])) { + if (!isset($functionCall->getArgs()[0])) { return $defaultReturnType; } - $argType = $scope->getType($functionCall->args[0]->value); - if ($argType instanceof MixedType) { + $algorithmType = $scope->getType($functionCall->getArgs()[0]->value); + if ($algorithmType instanceof MixedType) { return TypeUtils::toBenevolentUnion($defaultReturnType); } - $values = TypeUtils::getConstantStrings($argType); - if (count($values) !== 1) { + $constantAlgorithmTypes = TypeUtils::getConstantStrings($algorithmType); + + if ($constantAlgorithmTypes === []) { return TypeUtils::toBenevolentUnion($defaultReturnType); } - $string = $values[0]; - return in_array($string->getValue(), hash_algos(), true) ? new StringType() : new ConstantBooleanType(false); + $neverType = new NeverType(); + $falseType = new ConstantBooleanType(false); + $nonEmptyString = new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + + $invalidAlgorithmType = $this->phpVersion->throwsValueErrorForInternalFunctions() ? $neverType : $falseType; + $functionData = self::SUPPORTED_FUNCTIONS[strtolower($functionReflection->getName())]; + + $returnTypes = array_map( + function (ConstantStringType $type) use ($functionData, $nonEmptyString, $invalidAlgorithmType) { + $algorithm = strtolower($type->getValue()); + if (!in_array($algorithm, $this->hashAlgorithms, true)) { + return $invalidAlgorithmType; + } + if ($functionData['cryptographic'] && in_array($algorithm, self::NON_CRYPTOGRAPHIC_ALGORITHMS, true)) { + return $invalidAlgorithmType; + } + return $nonEmptyString; + }, + $constantAlgorithmTypes, + ); + + $returnType = TypeCombinator::union(...$returnTypes); + + if ($functionData['possiblyFalse'] && !$neverType->isSuperTypeOf($returnType)->yes()) { + $returnType = TypeCombinator::union($returnType, $falseType); + } + + return $returnType; } } diff --git a/src/Type/Php/HashHmacFunctionsReturnTypeExtension.php b/src/Type/Php/HashHmacFunctionsReturnTypeExtension.php deleted file mode 100644 index 208b55ed24..0000000000 --- a/src/Type/Php/HashHmacFunctionsReturnTypeExtension.php +++ /dev/null @@ -1,97 +0,0 @@ -getName(), ['hash_hmac', 'hash_hmac_file'], true); - } - - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type - { - if ($functionReflection->getName() === 'hash_hmac') { - $defaultReturnType = new StringType(); - } else { - $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - } - - if (!isset($functionCall->args[0])) { - return $defaultReturnType; - } - - $argType = $scope->getType($functionCall->args[0]->value); - if ($argType instanceof MixedType) { - return TypeUtils::toBenevolentUnion($defaultReturnType); - } - - $values = TypeUtils::getConstantStrings($argType); - if (count($values) !== 1) { - return TypeUtils::toBenevolentUnion($defaultReturnType); - } - $string = $values[0]; - - return in_array($string->getValue(), self::HMAC_ALGORITHMS, true) ? $defaultReturnType : new ConstantBooleanType(false); - } - -} diff --git a/src/Type/Php/HrtimeFunctionReturnTypeExtension.php b/src/Type/Php/HrtimeFunctionReturnTypeExtension.php index 44e8581750..05418cf926 100644 --- a/src/Type/Php/HrtimeFunctionReturnTypeExtension.php +++ b/src/Type/Php/HrtimeFunctionReturnTypeExtension.php @@ -8,13 +8,15 @@ use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function count; -class HrtimeFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class HrtimeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -27,11 +29,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $arrayType = new ConstantArrayType([new ConstantIntegerType(0), new ConstantIntegerType(1)], [new IntegerType(), new IntegerType()], 2); $numberType = TypeUtils::toBenevolentUnion(TypeCombinator::union(new IntegerType(), new FloatType())); - if (count($functionCall->args) < 1) { + if (count($functionCall->getArgs()) < 1) { return $arrayType; } - $argType = $scope->getType($functionCall->args[0]->value); + $argType = $scope->getType($functionCall->getArgs()[0]->value); $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($argType); $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($argType); $compareTypes = $isTrueType->compareTo($isFalseType); diff --git a/src/Type/Php/ImplodeFunctionReturnTypeExtension.php b/src/Type/Php/ImplodeFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..478eb9613b --- /dev/null +++ b/src/Type/Php/ImplodeFunctionReturnTypeExtension.php @@ -0,0 +1,105 @@ +getName(), [ + 'implode', + 'join', + ], true); + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): Type + { + $args = $functionCall->getArgs(); + if (count($args) === 1) { + $argType = $scope->getType($args[0]->value); + if ($argType->isArray()->yes()) { + return $this->implode($argType, new ConstantStringType('')); + } + } + + if (count($args) !== 2) { + return new StringType(); + } + + $separatorType = $scope->getType($args[0]->value); + $arrayType = $scope->getType($args[1]->value); + + return $this->implode($arrayType, $separatorType); + } + + private function implode(Type $arrayType, Type $separatorType): Type + { + if ($arrayType instanceof ConstantArrayType && $separatorType instanceof ConstantStringType) { + $constantType = $this->inferConstantType($arrayType, $separatorType); + if ($constantType !== null) { + return $constantType; + } + } + + $accessoryTypes = []; + if ($arrayType->isIterableAtLeastOnce()->yes()) { + if ($arrayType->getIterableValueType()->isNonEmptyString()->yes() || $separatorType->isNonEmptyString()->yes()) { + $accessoryTypes[] = new AccessoryNonEmptyStringType(); + } + } + + if ($arrayType->getIterableValueType()->isLiteralString()->yes() && $separatorType->isLiteralString()->yes()) { + $accessoryTypes[] = new AccessoryLiteralStringType(); + } + + if (count($accessoryTypes) > 0) { + $accessoryTypes[] = new StringType(); + return new IntersectionType($accessoryTypes); + } + + return new StringType(); + } + + private function inferConstantType(ConstantArrayType $arrayType, ConstantStringType $separatorType): ?Type + { + $strings = []; + foreach ($arrayType->getAllArrays() as $array) { + $valueTypes = $array->getValueTypes(); + + $arrayValues = []; + foreach ($valueTypes as $valueType) { + if (!$valueType instanceof ConstantScalarType) { + return null; + } + $arrayValues[] = $valueType->getValue(); + } + + $strings[] = new ConstantStringType(implode($separatorType->getValue(), $arrayValues)); + } + + return TypeCombinator::union(...$strings); + } + +} diff --git a/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php b/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php index 8310a0207f..98576400ef 100644 --- a/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php @@ -12,11 +12,13 @@ use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\TypeUtils; +use function count; +use function strtolower; class InArrayFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void { @@ -31,26 +33,26 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Func public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - if (count($node->args) < 3) { + if (count($node->getArgs()) < 3) { return new SpecifiedTypes(); } - $strictNodeType = $scope->getType($node->args[2]->value); + $strictNodeType = $scope->getType($node->getArgs()[2]->value); if (!(new ConstantBooleanType(true))->isSuperTypeOf($strictNodeType)->yes()) { return new SpecifiedTypes([], []); } - $arrayValueType = $scope->getType($node->args[1]->value)->getIterableValueType(); + $arrayValueType = $scope->getType($node->getArgs()[1]->value)->getIterableValueType(); if ( $context->truthy() || count(TypeUtils::getConstantScalars($arrayValueType)) > 0 ) { return $this->typeSpecifier->create( - $node->args[0]->value, + $node->getArgs()[0]->value, $arrayValueType, $context, false, - $scope + $scope, ); } diff --git a/src/Type/Php/IntdivThrowTypeExtension.php b/src/Type/Php/IntdivThrowTypeExtension.php index 5145a78920..8b97f9265a 100644 --- a/src/Type/Php/IntdivThrowTypeExtension.php +++ b/src/Type/Php/IntdivThrowTypeExtension.php @@ -2,6 +2,8 @@ namespace PHPStan\Type\Php; +use ArithmeticError; +use DivisionByZeroError; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; @@ -11,6 +13,8 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function count; +use const PHP_INT_MIN; class IntdivThrowTypeExtension implements DynamicFunctionThrowTypeExtension { @@ -22,12 +26,12 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type { - if (count($funcCall->args) < 2) { + if (count($funcCall->getArgs()) < 2) { return $functionReflection->getThrowType(); } $containsMin = false; - $valueType = $scope->getType($funcCall->args[0]->value); + $valueType = $scope->getType($funcCall->getArgs()[0]->value); foreach (TypeUtils::getConstantScalars($valueType) as $constantScalarType) { if ($constantScalarType->getValue() === PHP_INT_MIN) { $containsMin = true; @@ -41,10 +45,10 @@ public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflect } $divisionByZero = false; - $divisorType = $scope->getType($funcCall->args[1]->value); + $divisorType = $scope->getType($funcCall->getArgs()[1]->value); foreach (TypeUtils::getConstantScalars($divisorType) as $constantScalarType) { if ($containsMin && $constantScalarType->getValue() === -1) { - return new ObjectType(\ArithmeticError::class); + return new ObjectType(ArithmeticError::class); } if ($constantScalarType->getValue() === 0) { @@ -55,11 +59,11 @@ public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflect } if (!$divisorType instanceof NeverType) { - return new ObjectType($containsMin ? \ArithmeticError::class : \DivisionByZeroError::class); + return new ObjectType($containsMin ? ArithmeticError::class : DivisionByZeroError::class); } if ($divisionByZero) { - return new ObjectType(\DivisionByZeroError::class); + return new ObjectType(DivisionByZeroError::class); } return null; diff --git a/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php index 4ee1670161..2e03074b37 100644 --- a/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php @@ -2,78 +2,57 @@ namespace PHPStan\Type\Php; -use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\FuncCall; -use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Type\ClassStringType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FunctionTypeSpecifyingExtension; -use PHPStan\Type\Generic\GenericClassStringType; -use PHPStan\Type\ObjectType; -use PHPStan\Type\ObjectWithoutClassType; +use function count; +use function strtolower; class IsAFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; + + public function __construct( + private IsAFunctionTypeSpecifyingHelper $isAFunctionTypeSpecifyingHelper, + ) + { + } public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { return strtolower($functionReflection->getName()) === 'is_a' - && isset($node->args[0]) - && isset($node->args[1]) && !$context->null(); } public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); - } - - $classNameArgExpr = $node->args[1]->value; - $classNameArgExprType = $scope->getType($classNameArgExpr); - if ( - $classNameArgExpr instanceof ClassConstFetch - && $classNameArgExpr->class instanceof Name - && $classNameArgExpr->name instanceof \PhpParser\Node\Identifier - && strtolower($classNameArgExpr->name->name) === 'class' - ) { - $objectType = $scope->resolveTypeByName($classNameArgExpr->class); - $types = $this->typeSpecifier->create($node->args[0]->value, $objectType, $context, false, $scope); - } elseif ($classNameArgExprType instanceof ConstantStringType) { - $objectType = new ObjectType($classNameArgExprType->getValue()); - $types = $this->typeSpecifier->create($node->args[0]->value, $objectType, $context, false, $scope); - } elseif ($classNameArgExprType instanceof GenericClassStringType) { - $objectType = $classNameArgExprType->getGenericType(); - $types = $this->typeSpecifier->create($node->args[0]->value, $objectType, $context, false, $scope); - } elseif ($context->true()) { - $objectType = new ObjectWithoutClassType(); - $types = $this->typeSpecifier->create($node->args[0]->value, $objectType, $context, false, $scope); - } else { - $types = new SpecifiedTypes(); + if (count($node->getArgs()) < 2) { + return new SpecifiedTypes(); } + $objectOrClassType = $scope->getType($node->getArgs()[0]->value); + $classType = $scope->getType($node->getArgs()[1]->value); + $allowStringType = isset($node->getArgs()[2]) ? $scope->getType($node->getArgs()[2]->value) : new ConstantBooleanType(false); + $allowString = !$allowStringType->equals(new ConstantBooleanType(false)); - if (isset($node->args[2]) && $context->true()) { - if (!$scope->getType($node->args[2]->value)->isSuperTypeOf(new ConstantBooleanType(true))->no()) { - $types = $types->intersectWith($this->typeSpecifier->create( - $node->args[0]->value, - isset($objectType) ? new GenericClassStringType($objectType) : new ClassStringType(), - $context, - false, - $scope - )); - } + if (!$classType instanceof ConstantStringType && !$context->truthy()) { + return new SpecifiedTypes([], []); } - return $types; + return $this->typeSpecifier->create( + $node->getArgs()[0]->value, + $this->isAFunctionTypeSpecifyingHelper->determineType($objectOrClassType, $classType, $allowString, true), + $context, + false, + $scope, + ); } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php b/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php new file mode 100644 index 0000000000..c608156f84 --- /dev/null +++ b/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php @@ -0,0 +1,84 @@ +determineClassNameFromObjectOrClassType($objectOrClassType, $allowString); + + return TypeTraverser::map( + $classType, + static function (Type $type, callable $traverse) use ($objectOrClassTypeClassName, $allowString, $allowSameClass): Type { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); + } + if ($type instanceof ConstantStringType) { + if (!$allowSameClass && $type->getValue() === $objectOrClassTypeClassName) { + return new NeverType(); + } + if ($allowString) { + return TypeCombinator::union( + new ObjectType($type->getValue()), + new GenericClassStringType(new ObjectType($type->getValue())), + ); + } + + return new ObjectType($type->getValue()); + } + if ($type instanceof GenericClassStringType) { + if ($allowString) { + return TypeCombinator::union( + $type->getGenericType(), + $type, + ); + } + + return $type->getGenericType(); + } + if ($allowString) { + return TypeCombinator::union( + new ObjectWithoutClassType(), + new ClassStringType(), + ); + } + + return new ObjectWithoutClassType(); + }, + ); + } + + private function determineClassNameFromObjectOrClassType(Type $type, bool $allowString): ?string + { + if ($type instanceof TypeWithClassName) { + return $type->getClassName(); + } + + if ($allowString && $type instanceof ConstantStringType) { + return $type->getValue(); + } + + return null; + } + +} diff --git a/src/Type/Php/IsArrayFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsArrayFunctionTypeSpecifyingExtension.php index a0b82542a0..626e5de0eb 100644 --- a/src/Type/Php/IsArrayFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsArrayFunctionTypeSpecifyingExtension.php @@ -9,14 +9,16 @@ use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\ArrayType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\MixedType; +use function strtolower; class IsArrayFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { @@ -26,14 +28,14 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Func public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - if (!isset($node->args[0])) { + if (!isset($node->getArgs()[0])) { return new SpecifiedTypes(); } if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - return $this->typeSpecifier->create($node->args[0]->value, new ArrayType(new MixedType(), new MixedType()), $context, false, $scope); + return $this->typeSpecifier->create($node->getArgs()[0]->value, new ArrayType(new MixedType(), new MixedType()), $context, false, $scope); } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/src/Type/Php/IsBoolFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsBoolFunctionTypeSpecifyingExtension.php index 3753d2304a..44a9b8b088 100644 --- a/src/Type/Php/IsBoolFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsBoolFunctionTypeSpecifyingExtension.php @@ -9,13 +9,15 @@ use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\BooleanType; use PHPStan\Type\FunctionTypeSpecifyingExtension; +use function strtolower; class IsBoolFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { @@ -25,14 +27,14 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Func public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - if (!isset($node->args[0])) { + if (!isset($node->getArgs()[0])) { return new SpecifiedTypes(); } if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - return $this->typeSpecifier->create($node->args[0]->value, new BooleanType(), $context, false, $scope); + return $this->typeSpecifier->create($node->getArgs()[0]->value, new BooleanType(), $context, false, $scope); } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/src/Type/Php/IsCallableFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsCallableFunctionTypeSpecifyingExtension.php index eb051391d4..fc832c4737 100644 --- a/src/Type/Php/IsCallableFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsCallableFunctionTypeSpecifyingExtension.php @@ -12,20 +12,20 @@ use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\CallableType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\FunctionTypeSpecifyingExtension; +use function count; +use function strtolower; class IsCallableFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Type\Php\MethodExistsTypeSpecifyingExtension $methodExistsExtension; + private TypeSpecifier $typeSpecifier; - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; - - public function __construct(MethodExistsTypeSpecifyingExtension $methodExistsExtension) + public function __construct(private MethodExistsTypeSpecifyingExtension $methodExistsExtension) { - $this->methodExistsExtension = $methodExistsExtension; } public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool @@ -37,14 +37,14 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Func public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - if (!isset($node->args[0])) { + if (!isset($node->getArgs()[0])) { return new SpecifiedTypes(); } - $value = $node->args[0]->value; + $value = $node->getArgs()[0]->value; $valueType = $scope->getType($value); if ( $value instanceof Array_ @@ -53,7 +53,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n && !$valueType->isCallable()->no() ) { if ($value->items[0] === null || $value->items[1] === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $functionCall = new FuncCall(new Name('method_exists'), [ diff --git a/src/Type/Php/IsCountableFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsCountableFunctionTypeSpecifyingExtension.php index 50814695d3..c05579be5a 100644 --- a/src/Type/Php/IsCountableFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsCountableFunctionTypeSpecifyingExtension.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Php; +use Countable; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; @@ -9,16 +10,18 @@ use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\ArrayType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; use PHPStan\Type\UnionType; +use function strtolower; class IsCountableFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { @@ -29,22 +32,22 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Func public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - if (!isset($node->args[0])) { + if (!isset($node->getArgs()[0])) { return new SpecifiedTypes(); } return $this->typeSpecifier->create( - $node->args[0]->value, + $node->getArgs()[0]->value, new UnionType([ new ArrayType(new MixedType(), new MixedType()), - new ObjectType(\Countable::class), + new ObjectType(Countable::class), ]), $context, false, - $scope + $scope, ); } diff --git a/src/Type/Php/IsFloatFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsFloatFunctionTypeSpecifyingExtension.php index 33b021251e..de8c3f6cbd 100644 --- a/src/Type/Php/IsFloatFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsFloatFunctionTypeSpecifyingExtension.php @@ -9,13 +9,16 @@ use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\FloatType; use PHPStan\Type\FunctionTypeSpecifyingExtension; +use function in_array; +use function strtolower; class IsFloatFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { @@ -29,14 +32,14 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Func public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - if (!isset($node->args[0])) { + if (!isset($node->getArgs()[0])) { return new SpecifiedTypes(); } if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - return $this->typeSpecifier->create($node->args[0]->value, new FloatType(), $context, false, $scope); + return $this->typeSpecifier->create($node->getArgs()[0]->value, new FloatType(), $context, false, $scope); } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/src/Type/Php/IsIntFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsIntFunctionTypeSpecifyingExtension.php index b8d70648ae..a5504cf663 100644 --- a/src/Type/Php/IsIntFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsIntFunctionTypeSpecifyingExtension.php @@ -9,13 +9,16 @@ use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\IntegerType; +use function in_array; +use function strtolower; class IsIntFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { @@ -29,14 +32,14 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Func public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - if (!isset($node->args[0])) { + if (!isset($node->getArgs()[0])) { return new SpecifiedTypes(); } if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - return $this->typeSpecifier->create($node->args[0]->value, new IntegerType(), $context, false, $scope); + return $this->typeSpecifier->create($node->getArgs()[0]->value, new IntegerType(), $context, false, $scope); } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/src/Type/Php/IsIterableFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsIterableFunctionTypeSpecifyingExtension.php index 78d5337610..470d196894 100644 --- a/src/Type/Php/IsIterableFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsIterableFunctionTypeSpecifyingExtension.php @@ -9,14 +9,16 @@ use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\IterableType; use PHPStan\Type\MixedType; +use function strtolower; class IsIterableFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { @@ -27,14 +29,14 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Func public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - if (!isset($node->args[0])) { + if (!isset($node->getArgs()[0])) { return new SpecifiedTypes(); } - return $this->typeSpecifier->create($node->args[0]->value, new IterableType(new MixedType(), new MixedType()), $context, false, $scope); + return $this->typeSpecifier->create($node->getArgs()[0]->value, new IterableType(new MixedType(), new MixedType()), $context, false, $scope); } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/src/Type/Php/IsNullFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsNullFunctionTypeSpecifyingExtension.php index 85d26bb99b..403ee6c337 100644 --- a/src/Type/Php/IsNullFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsNullFunctionTypeSpecifyingExtension.php @@ -9,13 +9,15 @@ use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\NullType; +use function strtolower; class IsNullFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { @@ -26,14 +28,14 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Func public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - if (!isset($node->args[0])) { + if (!isset($node->getArgs()[0])) { return new SpecifiedTypes(); } - return $this->typeSpecifier->create($node->args[0]->value, new NullType(), $context, false, $scope); + return $this->typeSpecifier->create($node->getArgs()[0]->value, new NullType(), $context, false, $scope); } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/src/Type/Php/IsNumericFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsNumericFunctionTypeSpecifyingExtension.php index 9cf67e8359..3b16c1b51b 100644 --- a/src/Type/Php/IsNumericFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsNumericFunctionTypeSpecifyingExtension.php @@ -9,6 +9,7 @@ use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\FloatType; use PHPStan\Type\FunctionTypeSpecifyingExtension; @@ -20,7 +21,7 @@ class IsNumericFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { @@ -30,11 +31,11 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Func public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - if (!isset($node->args[0])) { + if (!isset($node->getArgs()[0])) { return new SpecifiedTypes(); } if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $numericTypes = [ @@ -49,7 +50,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n ]); } - return $this->typeSpecifier->create($node->args[0]->value, new UnionType($numericTypes), $context, false, $scope); + return $this->typeSpecifier->create($node->getArgs()[0]->value, new UnionType($numericTypes), $context, false, $scope); } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/src/Type/Php/IsObjectFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsObjectFunctionTypeSpecifyingExtension.php index 9d60ab9b3b..33bd282b65 100644 --- a/src/Type/Php/IsObjectFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsObjectFunctionTypeSpecifyingExtension.php @@ -9,13 +9,15 @@ use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\ObjectWithoutClassType; +use function strtolower; class IsObjectFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { @@ -26,14 +28,14 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Func public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - if (!isset($node->args[0])) { + if (!isset($node->getArgs()[0])) { return new SpecifiedTypes(); } - return $this->typeSpecifier->create($node->args[0]->value, new ObjectWithoutClassType(), $context, false, $scope); + return $this->typeSpecifier->create($node->getArgs()[0]->value, new ObjectWithoutClassType(), $context, false, $scope); } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/src/Type/Php/IsResourceFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsResourceFunctionTypeSpecifyingExtension.php index efaec426d8..392fb1159b 100644 --- a/src/Type/Php/IsResourceFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsResourceFunctionTypeSpecifyingExtension.php @@ -9,13 +9,15 @@ use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\ResourceType; +use function strtolower; class IsResourceFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { @@ -26,14 +28,14 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Func public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - if (!isset($node->args[0])) { + if (!isset($node->getArgs()[0])) { return new SpecifiedTypes(); } - return $this->typeSpecifier->create($node->args[0]->value, new ResourceType(), $context, false, $scope); + return $this->typeSpecifier->create($node->getArgs()[0]->value, new ResourceType(), $context, false, $scope); } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/src/Type/Php/IsScalarFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsScalarFunctionTypeSpecifyingExtension.php index 4bcdd7c962..33ce803d01 100644 --- a/src/Type/Php/IsScalarFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsScalarFunctionTypeSpecifyingExtension.php @@ -9,6 +9,7 @@ use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\BooleanType; use PHPStan\Type\FloatType; use PHPStan\Type\FunctionTypeSpecifyingExtension; @@ -19,7 +20,7 @@ class IsScalarFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { @@ -30,14 +31,14 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Func public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - if (!isset($node->args[0])) { + if (!isset($node->getArgs()[0])) { return new SpecifiedTypes(); } - return $this->typeSpecifier->create($node->args[0]->value, new UnionType([ + return $this->typeSpecifier->create($node->getArgs()[0]->value, new UnionType([ new StringType(), new IntegerType(), new FloatType(), diff --git a/src/Type/Php/IsStringFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsStringFunctionTypeSpecifyingExtension.php index f589ec523c..7e307545b6 100644 --- a/src/Type/Php/IsStringFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsStringFunctionTypeSpecifyingExtension.php @@ -9,13 +9,15 @@ use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\StringType; +use function strtolower; class IsStringFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { @@ -26,14 +28,14 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Func public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { if ($context->null()) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } - if (!isset($node->args[0])) { + if (!isset($node->getArgs()[0])) { return new SpecifiedTypes(); } - return $this->typeSpecifier->create($node->args[0]->value, new StringType(), $context, false, $scope); + return $this->typeSpecifier->create($node->getArgs()[0]->value, new StringType(), $context, false, $scope); } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php index e692d24ed1..2740401e4f 100644 --- a/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php @@ -9,27 +9,22 @@ use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Type\ClassStringType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FunctionTypeSpecifyingExtension; -use PHPStan\Type\Generic\GenericClassStringType; -use PHPStan\Type\IntersectionType; -use PHPStan\Type\MixedType; -use PHPStan\Type\NeverType; -use PHPStan\Type\ObjectType; -use PHPStan\Type\ObjectWithoutClassType; -use PHPStan\Type\StringType; -use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use PHPStan\Type\TypeTraverser; -use PHPStan\Type\TypeWithClassName; -use PHPStan\Type\UnionType; +use function count; +use function strtolower; class IsSubclassOfFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; + private TypeSpecifier $typeSpecifier; + + public function __construct( + private IsAFunctionTypeSpecifyingHelper $isAFunctionTypeSpecifyingHelper, + ) + { + } public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { @@ -39,72 +34,24 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Func public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - if (count($node->args) < 2) { + if (count($node->getArgs()) < 2) { return new SpecifiedTypes(); } - $objectType = $scope->getType($node->args[0]->value); - $classType = $scope->getType($node->args[1]->value); - $allowStringType = isset($node->args[2]) ? $scope->getType($node->args[2]->value) : new ConstantBooleanType(true); + $objectOrClassType = $scope->getType($node->getArgs()[0]->value); + $classType = $scope->getType($node->getArgs()[1]->value); + $allowStringType = isset($node->getArgs()[2]) ? $scope->getType($node->getArgs()[2]->value) : new ConstantBooleanType(true); $allowString = !$allowStringType->equals(new ConstantBooleanType(false)); - if (!$classType instanceof ConstantStringType) { - if ($context->truthy()) { - if ($allowString) { - $type = TypeCombinator::union( - new ObjectWithoutClassType(), - new ClassStringType() - ); - } else { - $type = new ObjectWithoutClassType(); - } - - return $this->typeSpecifier->create( - $node->args[0]->value, - $type, - $context, - false, - $scope - ); - } - - return new SpecifiedTypes(); + if (!$classType instanceof ConstantStringType && !$context->truthy()) { + return new SpecifiedTypes([], []); } - $type = TypeTraverser::map($objectType, static function (Type $type, callable $traverse) use ($classType, $allowString): Type { - if ($type instanceof UnionType) { - return $traverse($type); - } - if ($type instanceof IntersectionType) { - return $traverse($type); - } - if ($allowString) { - if ($type instanceof StringType) { - return new GenericClassStringType(new ObjectType($classType->getValue())); - } - } - if ($type instanceof ObjectWithoutClassType || $type instanceof TypeWithClassName) { - return new ObjectType($classType->getValue()); - } - if ($type instanceof MixedType) { - $objectType = new ObjectType($classType->getValue()); - if ($allowString) { - return TypeCombinator::union( - new GenericClassStringType($objectType), - $objectType - ); - } - - return $objectType; - } - return new NeverType(); - }); - return $this->typeSpecifier->create( - $node->args[0]->value, - $type, + $node->getArgs()[0]->value, + $this->isAFunctionTypeSpecifyingHelper->determineType($objectOrClassType, $classType, $allowString, false), $context, false, - $scope + $scope, ); } diff --git a/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php b/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..fe8be93631 --- /dev/null +++ b/src/Type/Php/IteratorToArrayFunctionReturnTypeExtension.php @@ -0,0 +1,49 @@ +getName()) === 'iterator_to_array'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + { + $arguments = $functionCall->getArgs(); + + if ($arguments === []) { + return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + } + + $traversableType = $scope->getType($arguments[0]->value); + $arrayKeyType = $traversableType->getIterableKeyType(); + + if (isset($arguments[1])) { + $preserveKeysType = $scope->getType($arguments[1]->value); + + if ($preserveKeysType instanceof ConstantBooleanType && !$preserveKeysType->getValue()) { + $arrayKeyType = new IntegerType(); + } + } + + return new ArrayType( + $arrayKeyType, + $traversableType->getIterableValueType(), + ); + } + +} diff --git a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php index e2697da0a0..15de31eda3 100644 --- a/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php +++ b/src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php @@ -13,10 +13,12 @@ use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function in_array; -class JsonThrowOnErrorDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class JsonThrowOnErrorDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { /** @var array */ @@ -25,15 +27,12 @@ class JsonThrowOnErrorDynamicReturnTypeExtension implements \PHPStan\Type\Dynami 'json_decode' => 3, ]; - private ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct(private ReflectionProvider $reflectionProvider) { - $this->reflectionProvider = $reflectionProvider; } public function isFunctionSupported( - FunctionReflection $functionReflection + FunctionReflection $functionReflection, ): bool { return $this->reflectionProvider->hasConstant(new FullyQualified('JSON_THROW_ON_ERROR'), null) && in_array( @@ -42,23 +41,23 @@ public function isFunctionSupported( 'json_encode', 'json_decode', ], - true + true, ); } public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope + Scope $scope, ): Type { $argumentPosition = $this->argumentPositions[$functionReflection->getName()]; $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - if (!isset($functionCall->args[$argumentPosition])) { + if (!isset($functionCall->getArgs()[$argumentPosition])) { return $defaultReturnType; } - $optionsExpr = $functionCall->args[$argumentPosition]->value; + $optionsExpr = $functionCall->getArgs()[$argumentPosition]->value; $constrictedReturnType = TypeCombinator::remove($defaultReturnType, new ConstantBooleanType(false)); if ($this->isBitwiseOrWithJsonThrowOnError($optionsExpr, $scope)) { return $constrictedReturnType; diff --git a/src/Type/Php/JsonThrowTypeExtension.php b/src/Type/Php/JsonThrowTypeExtension.php index bcb7bd4ee3..21c4cc6920 100644 --- a/src/Type/Php/JsonThrowTypeExtension.php +++ b/src/Type/Php/JsonThrowTypeExtension.php @@ -14,6 +14,7 @@ use PHPStan\Type\DynamicFunctionThrowTypeExtension; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use function in_array; class JsonThrowTypeExtension implements DynamicFunctionThrowTypeExtension { @@ -24,15 +25,12 @@ class JsonThrowTypeExtension implements DynamicFunctionThrowTypeExtension 'json_decode' => 3, ]; - private ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct(private ReflectionProvider $reflectionProvider) { - $this->reflectionProvider = $reflectionProvider; } public function isFunctionSupported( - FunctionReflection $functionReflection + FunctionReflection $functionReflection, ): bool { return $this->reflectionProvider->hasConstant(new Name\FullyQualified('JSON_THROW_ON_ERROR'), null) && in_array( @@ -41,22 +39,22 @@ public function isFunctionSupported( 'json_encode', 'json_decode', ], - true + true, ); } public function getThrowTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope + Scope $scope, ): ?Type { $argumentPosition = $this->argumentPositions[$functionReflection->getName()]; - if (!isset($functionCall->args[$argumentPosition])) { + if (!isset($functionCall->getArgs()[$argumentPosition])) { return null; } - $optionsExpr = $functionCall->args[$argumentPosition]->value; + $optionsExpr = $functionCall->getArgs()[$argumentPosition]->value; if ($this->isBitwiseOrWithJsonThrowOnError($optionsExpr, $scope)) { return new ObjectType('JsonException'); } diff --git a/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php b/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php index 1369e545d6..7b84444465 100644 --- a/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php +++ b/src/Type/Php/MbConvertEncodingFunctionReturnTypeExtension.php @@ -24,15 +24,15 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope + Scope $scope, ): Type { $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - if (!isset($functionCall->args[0])) { + if (!isset($functionCall->getArgs()[0])) { return $defaultReturnType; } - $argType = $scope->getType($functionCall->args[0]->value); + $argType = $scope->getType($functionCall->getArgs()[0]->value); $isString = (new StringType())->isSuperTypeOf($argType); $isArray = (new ArrayType(new MixedType(), new MixedType()))->isSuperTypeOf($argType); $compare = $isString->compareTo($isArray); diff --git a/src/Type/Php/MbFunctionsReturnTypeExtension.php b/src/Type/Php/MbFunctionsReturnTypeExtension.php index 0b98c19e43..7de6a457f2 100644 --- a/src/Type/Php/MbFunctionsReturnTypeExtension.php +++ b/src/Type/Php/MbFunctionsReturnTypeExtension.php @@ -4,18 +4,32 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\NeverType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; +use function array_key_exists; +use function array_map; +use function array_merge; +use function array_unique; +use function count; +use function function_exists; +use function in_array; +use function mb_encoding_aliases; +use function mb_list_encodings; +use function strtoupper; -class MbFunctionsReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class MbFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeExtension { /** @var string[] */ @@ -32,14 +46,14 @@ class MbFunctionsReturnTypeExtension implements \PHPStan\Type\DynamicFunctionRet 'mb_ord' => 2, ]; - public function __construct() + public function __construct(private PhpVersion $phpVersion) { $supportedEncodings = []; if (function_exists('mb_list_encodings')) { foreach (mb_list_encodings() as $encoding) { $aliases = mb_encoding_aliases($encoding); if ($aliases === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $supportedEncodings = array_merge($supportedEncodings, $aliases, [$encoding]); } @@ -62,23 +76,26 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $returnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); $positionEncodingParam = $this->encodingPositionMap[$functionReflection->getName()]; - if (count($functionCall->args) < $positionEncodingParam) { + if (count($functionCall->getArgs()) < $positionEncodingParam) { return TypeCombinator::remove($returnType, new BooleanType()); } - $strings = TypeUtils::getConstantStrings($scope->getType($functionCall->args[$positionEncodingParam - 1]->value)); - $results = array_unique(array_map(function (ConstantStringType $encoding): bool { - return $this->isSupportedEncoding($encoding->getValue()); - }, $strings)); + $strings = TypeUtils::getConstantStrings($scope->getType($functionCall->getArgs()[$positionEncodingParam - 1]->value)); + $results = array_unique(array_map(fn (ConstantStringType $encoding): bool => $this->isSupportedEncoding($encoding->getValue()), $strings)); if ($returnType->equals(new UnionType([new StringType(), new BooleanType()]))) { return count($results) === 1 ? new ConstantBooleanType($results[0]) : new BooleanType(); } if (count($results) === 1) { + $invalidEncodingReturn = new ConstantBooleanType(false); + if ($this->phpVersion->throwsOnInvalidMbStringEncoding()) { + $invalidEncodingReturn = new NeverType(); + } + return $results[0] ? TypeCombinator::remove($returnType, new ConstantBooleanType(false)) - : new ConstantBooleanType(false); + : $invalidEncodingReturn; } return $returnType; diff --git a/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php b/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..a379b602a9 --- /dev/null +++ b/src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php @@ -0,0 +1,138 @@ +getName() === 'mb_substitute_character'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + { + $minCodePoint = $this->phpVersion->getVersionId() < 80000 ? 1 : 0; + $maxCodePoint = $this->phpVersion->supportsAllUnicodeScalarCodePointsInMbSubstituteCharacter() ? 0x10FFFF : 0xFFFE; + $ranges = []; + + if ($this->phpVersion->supportsAllUnicodeScalarCodePointsInMbSubstituteCharacter()) { + // Surrogates aren't valid in PHP 7.2+ + $ranges[] = IntegerRangeType::fromInterval($minCodePoint, 0xD7FF); + $ranges[] = IntegerRangeType::fromInterval(0xE000, $maxCodePoint); + } else { + $ranges[] = IntegerRangeType::fromInterval($minCodePoint, $maxCodePoint); + } + + if (!isset($functionCall->getArgs()[0])) { + return TypeCombinator::union( + new ConstantStringType('none'), + new ConstantStringType('long'), + new ConstantStringType('entity'), + ...$ranges, + ); + } + + $argType = $scope->getType($functionCall->getArgs()[0]->value); + $isString = (new StringType())->isSuperTypeOf($argType); + $isNull = (new NullType())->isSuperTypeOf($argType); + $isInteger = (new IntegerType())->isSuperTypeOf($argType); + + if ($isString->no() && $isNull->no() && $isInteger->no()) { + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return new NeverType(); + } + + return new BooleanType(); + } + + if ($isInteger->yes()) { + $invalidRanges = []; + + foreach ($ranges as $range) { + $isInRange = $range->isSuperTypeOf($argType); + + if ($isInRange->yes()) { + return new ConstantBooleanType(true); + } + + $invalidRanges[] = $isInRange->no(); + } + + if ($argType instanceof ConstantIntegerType || !in_array(false, $invalidRanges, true)) { + if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { + return new NeverType(); + } + + return new ConstantBooleanType(false); + } + } elseif ($isString->yes()) { + if ($argType->isNonEmptyString()->no()) { + // The empty string was a valid alias for "none" in PHP < 8. + if ($this->phpVersion->isEmptyStringValidAliasForNoneInMbSubstituteCharacter()) { + return new ConstantBooleanType(true); + } + + return new NeverType(); + } + + if (!$this->phpVersion->isNumericStringValidArgInMbSubstituteCharacter() && $argType->isNumericString()->yes()) { + return new NeverType(); + } + + if ($argType instanceof ConstantStringType) { + $value = strtolower($argType->getValue()); + + if ($value === 'none' || $value === 'long' || $value === 'entity') { + return new ConstantBooleanType(true); + } + + if ($argType->isNumericString()->yes()) { + $codePoint = (int) $value; + $isValid = $codePoint >= $minCodePoint && $codePoint <= $maxCodePoint; + + if ($this->phpVersion->supportsAllUnicodeScalarCodePointsInMbSubstituteCharacter()) { + $isValid = $isValid && ($codePoint < 0xD800 || $codePoint > 0xDFFF); + } + + return new ConstantBooleanType($isValid); + } + + if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { + return new NeverType(); + } + + return new ConstantBooleanType(false); + } + } elseif ($isNull->yes()) { + // The $substitute_character arg is nullable in PHP 8+ + return new ConstantBooleanType($this->phpVersion->isNullValidArgInMbSubstituteCharacter()); + } + + return new BooleanType(); + } + +} diff --git a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php index 2a5228d7d9..d11b1af70b 100644 --- a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php @@ -18,6 +18,7 @@ use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; use PHPStan\Type\UnionType; +use function count; class MethodExistsTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { @@ -32,35 +33,35 @@ public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void public function isFunctionSupported( FunctionReflection $functionReflection, FuncCall $node, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): bool { return $functionReflection->getName() === 'method_exists' && $context->truthy() - && count($node->args) >= 2; + && count($node->getArgs()) >= 2; } public function specifyTypes( FunctionReflection $functionReflection, FuncCall $node, Scope $scope, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): SpecifiedTypes { - $objectType = $scope->getType($node->args[0]->value); + $objectType = $scope->getType($node->getArgs()[0]->value); if (!$objectType instanceof ObjectType) { if ((new StringType())->isSuperTypeOf($objectType)->yes()) { return new SpecifiedTypes([], []); } } - $methodNameType = $scope->getType($node->args[1]->value); + $methodNameType = $scope->getType($node->getArgs()[1]->value); if (!$methodNameType instanceof ConstantStringType) { return new SpecifiedTypes([], []); } return $this->typeSpecifier->create( - $node->args[0]->value, + $node->getArgs()[0]->value, new UnionType([ new IntersectionType([ new ObjectWithoutClassType(), @@ -70,7 +71,7 @@ public function specifyTypes( ]), $context, false, - $scope + $scope, ); } diff --git a/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php b/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php index 15ef4772dd..de55a206d8 100644 --- a/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php +++ b/src/Type/Php/MicrotimeFunctionReturnTypeExtension.php @@ -7,13 +7,15 @@ 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; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; +use function count; -class MicrotimeFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class MicrotimeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -23,11 +25,11 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - if (count($functionCall->args) < 1) { + if (count($functionCall->getArgs()) < 1) { return new StringType(); } - $argType = $scope->getType($functionCall->args[0]->value); + $argType = $scope->getType($functionCall->getArgs()[0]->value); $isTrueType = (new ConstantBooleanType(true))->isSuperTypeOf($argType); $isFalseType = (new ConstantBooleanType(false))->isSuperTypeOf($argType); $compareTypes = $isTrueType->compareTo($isFalseType); diff --git a/src/Type/Php/MinMaxFunctionReturnTypeExtension.php b/src/Type/Php/MinMaxFunctionReturnTypeExtension.php index e710b9a8e3..4c346d3459 100644 --- a/src/Type/Php/MinMaxFunctionReturnTypeExtension.php +++ b/src/Type/Php/MinMaxFunctionReturnTypeExtension.php @@ -2,7 +2,9 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Expr\BinaryOp\Smaller; use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Expr\Ternary; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; @@ -10,12 +12,14 @@ use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\ConstantScalarType; use PHPStan\Type\ConstantType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; +use function count; -class MinMaxFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class MinMaxFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { /** @var string[] */ @@ -31,12 +35,12 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - if (!isset($functionCall->args[0])) { + if (!isset($functionCall->getArgs()[0])) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } - if (count($functionCall->args) === 1) { - $argType = $scope->getType($functionCall->args[0]->value); + if (count($functionCall->getArgs()) === 1) { + $argType = $scope->getType($functionCall->getArgs()[0]->value); if ($argType->isArray()->yes()) { $isIterable = $argType->isIterableAtLeastOnce(); if ($isIterable->no()) { @@ -57,15 +61,40 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $this->processType( $functionReflection->getName(), - $argumentTypes + $argumentTypes, ); } return new ErrorType(); } + // rewrite min($x, $y) as $x < $y ? $x : $y + // we don't handle arrays, which have different semantics + $functionName = $functionReflection->getName(); + $args = $functionCall->getArgs(); + if (count($functionCall->getArgs()) === 2) { + $argType0 = $scope->getType($args[0]->value); + $argType1 = $scope->getType($args[1]->value); + + if ($argType0->isArray()->no() && $argType1->isArray()->no()) { + if ($functionName === 'min') { + return $scope->getType(new Ternary( + new Smaller($args[0]->value, $args[1]->value), + $args[0]->value, + $args[1]->value, + )); + } elseif ($functionName === 'max') { + return $scope->getType(new Ternary( + new Smaller($args[0]->value, $args[1]->value), + $args[1]->value, + $args[0]->value, + )); + } + } + } + $argumentTypes = []; - foreach ($functionCall->args as $arg) { + foreach ($functionCall->getArgs() as $arg) { $argType = $scope->getType($arg->value); if ($arg->unpack) { $iterableValueType = $argType->getIterableValueType(); @@ -83,19 +112,17 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } return $this->processType( - $functionReflection->getName(), - $argumentTypes + $functionName, + $argumentTypes, ); } /** - * @param string $functionName - * @param \PHPStan\Type\Type[] $types - * @return Type + * @param Type[] $types */ private function processType( string $functionName, - array $types + array $types, ): Type { $resultType = null; @@ -130,7 +157,7 @@ private function processType( private function compareTypes( Type $firstType, - Type $secondType + Type $secondType, ): ?Type { if ( diff --git a/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php b/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php new file mode 100644 index 0000000000..94055e1499 --- /dev/null +++ b/src/Type/Php/NonEmptyStringFunctionsReturnTypeExtension.php @@ -0,0 +1,67 @@ +getName(), [ + 'addslashes', + 'addcslashes', + 'escapeshellarg', + 'escapeshellcmd', + 'strtoupper', + 'strtolower', + 'mb_strtoupper', + 'mb_strtolower', + 'lcfirst', + 'ucfirst', + 'ucwords', + 'htmlspecialchars', + 'htmlentities', + 'urlencode', + 'urldecode', + 'preg_quote', + 'rawurlencode', + 'rawurldecode', + 'vsprintf', + ], true); + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): Type + { + $args = $functionCall->getArgs(); + if (count($args) === 0) { + return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + } + + $argType = $scope->getType($args[0]->value); + if ($argType->isNonEmptyString()->yes()) { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + + return new StringType(); + } + +} diff --git a/src/Type/Php/NumberFormatFunctionDynamicReturnTypeExtension.php b/src/Type/Php/NumberFormatFunctionDynamicReturnTypeExtension.php index 0dc7fb0cc9..1a564b358c 100644 --- a/src/Type/Php/NumberFormatFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/NumberFormatFunctionDynamicReturnTypeExtension.php @@ -8,11 +8,13 @@ use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ConstantScalarType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use function in_array; -final class NumberFormatFunctionDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +final class NumberFormatFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -23,12 +25,12 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { $stringType = new StringType(); - if (!isset($functionCall->args[3])) { + if (!isset($functionCall->getArgs()[3])) { return $stringType; } - $thousandsType = $scope->getType($functionCall->args[3]->value); - $decimalType = $scope->getType($functionCall->args[2]->value); + $thousandsType = $scope->getType($functionCall->getArgs()[3]->value); + $decimalType = $scope->getType($functionCall->getArgs()[2]->value); if (!$thousandsType instanceof ConstantStringType || $thousandsType->getValue() !== '') { return $stringType; diff --git a/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php index 724ed2dca8..307ddb4549 100644 --- a/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ParseUrlFunctionDynamicReturnTypeExtension.php @@ -6,6 +6,7 @@ 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; @@ -17,6 +18,17 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use ValueError; +use function count; +use function parse_url; +use const PHP_URL_FRAGMENT; +use const PHP_URL_HOST; +use const PHP_URL_PASS; +use const PHP_URL_PATH; +use const PHP_URL_PORT; +use const PHP_URL_QUERY; +use const PHP_URL_SCHEME; +use const PHP_URL_USER; final class ParseUrlFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -36,19 +48,17 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - $defaultReturnType = ParametersAcceptorSelector::selectSingle( - $functionReflection->getVariants() - )->getReturnType(); - - if (count($functionCall->args) < 1) { - return $defaultReturnType; + if (count($functionCall->getArgs()) < 1) { + return ParametersAcceptorSelector::selectSingle( + $functionReflection->getVariants(), + )->getReturnType(); } $this->cacheReturnTypes(); - $urlType = $scope->getType($functionCall->args[0]->value); - if (count($functionCall->args) > 1) { - $componentType = $scope->getType($functionCall->args[1]->value); + $urlType = $scope->getType($functionCall->getArgs()[0]->value); + if (count($functionCall->getArgs()) > 1) { + $componentType = $scope->getType($functionCall->getArgs()[1]->value); if (!$componentType instanceof ConstantType) { return $this->createAllComponentsReturnType(); @@ -57,7 +67,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $componentType = $componentType->toInteger(); if (!$componentType instanceof ConstantIntegerType) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } } else { $componentType = new ConstantIntegerType(-1); @@ -66,7 +76,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($urlType instanceof ConstantStringType) { try { $result = @parse_url($urlType->getValue(), $componentType->getValue()); - } catch (\ValueError $e) { + } catch (ValueError) { return new ConstantBooleanType(false); } @@ -90,7 +100,7 @@ private function createAllComponentsReturnType(): Type $builder = ConstantArrayTypeBuilder::createEmpty(); if ($this->componentTypesPairedStrings === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } foreach ($this->componentTypesPairedStrings as $componentName => $componentValueType) { diff --git a/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php b/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php index dbb09f1a8a..ecbf504a31 100644 --- a/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php @@ -2,16 +2,19 @@ namespace PHPStan\Type\Php; +use PhpParser\Node; 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; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use function count; -class PathinfoFunctionDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class PathinfoFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -21,11 +24,11 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, - \PhpParser\Node\Expr\FuncCall $functionCall, - Scope $scope + Node\Expr\FuncCall $functionCall, + Scope $scope, ): Type { - $argsCount = count($functionCall->args); + $argsCount = count($functionCall->getArgs()); if ($argsCount === 0) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } elseif ($argsCount === 1) { @@ -34,8 +37,8 @@ public function getTypeFromFunctionCall( $builder = ConstantArrayTypeBuilder::createFromConstantArray( new ConstantArrayType( [new ConstantStringType('dirname'), new ConstantStringType('basename'), new ConstantStringType('filename')], - [$stringType, $stringType, $stringType] - ) + [$stringType, $stringType, $stringType], + ), ); $builder->setOffsetValueType(new ConstantStringType('extension'), $stringType, true); diff --git a/src/Type/Php/PowFunctionReturnTypeExtension.php b/src/Type/Php/PowFunctionReturnTypeExtension.php index 9e84218270..bbae101311 100644 --- a/src/Type/Php/PowFunctionReturnTypeExtension.php +++ b/src/Type/Php/PowFunctionReturnTypeExtension.php @@ -13,6 +13,7 @@ use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function count; class PowFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -28,12 +29,12 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, new FloatType(), new IntegerType(), ]); - if (count($functionCall->args) < 2) { + if (count($functionCall->getArgs()) < 2) { return $defaultReturnType; } - $firstArgType = $scope->getType($functionCall->args[0]->value); - $secondArgType = $scope->getType($functionCall->args[1]->value); + $firstArgType = $scope->getType($functionCall->getArgs()[0]->value); + $secondArgType = $scope->getType($functionCall->getArgs()[1]->value); if ($firstArgType instanceof MixedType || $secondArgType instanceof MixedType) { return $defaultReturnType; } diff --git a/src/Type/Php/PregFilterFunctionReturnTypeExtension.php b/src/Type/Php/PregFilterFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..3e158d1bcc --- /dev/null +++ b/src/Type/Php/PregFilterFunctionReturnTypeExtension.php @@ -0,0 +1,47 @@ +getName() === 'preg_filter'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + { + $defaultReturn = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + + $argsCount = count($functionCall->getArgs()); + if ($argsCount < 3) { + return $defaultReturn; + } + + $subjectType = $scope->getType($functionCall->getArgs()[2]->value); + + if ($subjectType->isArray()->yes()) { + return new ArrayType(new IntegerType(), new StringType()); + } + if ($subjectType->isString()->yes()) { + return new UnionType([new StringType(), new NullType()]); + } + + return $defaultReturn; + } + +} diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php index 912363f117..53f9ba6e66 100644 --- a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -3,30 +3,33 @@ namespace PHPStan\Type\Php; use PhpParser\Node\Arg; +use PhpParser\Node\Expr; +use PhpParser\Node\Expr\BinaryOp\BitwiseOr; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function sprintf; +use function strtolower; class PregSplitDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - private ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct(private ReflectionProvider $reflectionProvider) { - $this->reflectionProvider = $reflectionProvider; } @@ -38,12 +41,12 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - $flagsArg = $functionCall->args[3] ?? null; + $flagsArg = $functionCall->getArgs()[3] ?? null; if ($this->hasFlag($this->getConstant('PREG_SPLIT_OFFSET_CAPTURE'), $flagsArg, $scope)) { $type = new ArrayType( new IntegerType(), - new ConstantArrayType([new ConstantIntegerType(0), new ConstantIntegerType(1)], [new StringType(), new IntegerType()]) + new ConstantArrayType([new ConstantIntegerType(0), new ConstantIntegerType(1)], [new StringType(), IntegerRangeType::fromInterval(0, null)]), ); return TypeCombinator::union($type, new ConstantBooleanType(false)); } @@ -52,13 +55,25 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } - private function hasFlag(int $flag, ?Arg $expression, Scope $scope): bool + private function hasFlag(int $flag, ?Arg $arg, Scope $scope): bool { - if ($expression === null) { + if ($arg === null) { return false; } - $type = $scope->getType($expression->value); + return $this->isConstantFlag($flag, $arg->value, $scope); + } + + private function isConstantFlag(int $flag, Expr $expression, Scope $scope): bool + { + if ($expression instanceof BitwiseOr) { + $left = $expression->left; + $right = $expression->right; + + return $this->isConstantFlag($flag, $left, $scope) || $this->isConstantFlag($flag, $right, $scope); + } + + $type = $scope->getType($expression); return $type instanceof ConstantIntegerType && ($type->getValue() & $flag) === $flag; } @@ -68,7 +83,7 @@ private function getConstant(string $constantName): int $constant = $this->reflectionProvider->getConstant(new Name($constantName), null); $valueType = $constant->getValueType(); if (!$valueType instanceof ConstantIntegerType) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Constant %s does not have integer type.', $constantName)); + throw new ShouldNotHappenException(sprintf('Constant %s does not have integer type.', $constantName)); } return $valueType->getValue(); diff --git a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php index 7ff323cfa2..dbee387fc5 100644 --- a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php @@ -17,17 +17,15 @@ use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\IntersectionType; use PHPStan\Type\ObjectWithoutClassType; +use function count; class PropertyExistsTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private PropertyReflectionFinder $propertyReflectionFinder; - private TypeSpecifier $typeSpecifier; - public function __construct(PropertyReflectionFinder $propertyReflectionFinder) + public function __construct(private PropertyReflectionFinder $propertyReflectionFinder) { - $this->propertyReflectionFinder = $propertyReflectionFinder; } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void @@ -38,33 +36,33 @@ public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void public function isFunctionSupported( FunctionReflection $functionReflection, FuncCall $node, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): bool { return $functionReflection->getName() === 'property_exists' && $context->truthy() - && count($node->args) >= 2; + && count($node->getArgs()) >= 2; } public function specifyTypes( FunctionReflection $functionReflection, FuncCall $node, Scope $scope, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): SpecifiedTypes { - $propertyNameType = $scope->getType($node->args[1]->value); + $propertyNameType = $scope->getType($node->getArgs()[1]->value); if (!$propertyNameType instanceof ConstantStringType) { return new SpecifiedTypes([], []); } - $objectType = $scope->getType($node->args[0]->value); + $objectType = $scope->getType($node->getArgs()[0]->value); if ($objectType instanceof ConstantStringType) { return new SpecifiedTypes([], []); } elseif ((new ObjectWithoutClassType())->isSuperTypeOf($objectType)->yes()) { $propertyNode = new PropertyFetch( - $node->args[0]->value, - new Identifier($propertyNameType->getValue()) + $node->getArgs()[0]->value, + new Identifier($propertyNameType->getValue()), ); } else { return new SpecifiedTypes([], []); @@ -78,14 +76,14 @@ public function specifyTypes( } return $this->typeSpecifier->create( - $node->args[0]->value, + $node->getArgs()[0]->value, new IntersectionType([ new ObjectWithoutClassType(), new HasPropertyType($propertyNameType->getValue()), ]), $context, false, - $scope + $scope, ); } diff --git a/src/Type/Php/RandomIntFunctionReturnTypeExtension.php b/src/Type/Php/RandomIntFunctionReturnTypeExtension.php index 21faac5e72..5b71dbf433 100644 --- a/src/Type/Php/RandomIntFunctionReturnTypeExtension.php +++ b/src/Type/Php/RandomIntFunctionReturnTypeExtension.php @@ -7,26 +7,37 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; +use function array_map; +use function assert; +use function count; +use function in_array; +use function max; +use function min; -class RandomIntFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class RandomIntFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return $functionReflection->getName() === 'random_int'; + return in_array($functionReflection->getName(), ['random_int', 'rand'], true); } public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - if (count($functionCall->args) < 2) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + if ($functionReflection->getName() === 'rand' && count($functionCall->getArgs()) === 0) { + return IntegerRangeType::fromInterval(0, null); } - $minType = $scope->getType($functionCall->args[0]->value)->toInteger(); - $maxType = $scope->getType($functionCall->args[1]->value)->toInteger(); + if (count($functionCall->getArgs()) < 2) { + return ParametersAcceptorSelector::selectFromArgs($scope, $functionCall->getArgs(), $functionReflection->getVariants())->getReturnType(); + } + + $minType = $scope->getType($functionCall->getArgs()[0]->value)->toInteger(); + $maxType = $scope->getType($functionCall->getArgs()[1]->value)->toInteger(); return $this->createRange($minType, $maxType); } @@ -43,7 +54,7 @@ static function (Type $type): ?int { } return null; }, - $minType instanceof UnionType ? $minType->getTypes() : [$minType] + $minType instanceof UnionType ? $minType->getTypes() : [$minType], ); $maxValues = array_map( @@ -56,7 +67,7 @@ static function (Type $type): ?int { } return null; }, - $maxType instanceof UnionType ? $maxType->getTypes() : [$maxType] + $maxType instanceof UnionType ? $maxType->getTypes() : [$maxType], ); assert(count($minValues) > 0); @@ -64,7 +75,7 @@ static function (Type $type): ?int { return IntegerRangeType::fromInterval( in_array(null, $minValues, true) ? null : min($minValues), - in_array(null, $maxValues, true) ? null : max($maxValues) + in_array(null, $maxValues, true) ? null : max($maxValues), ); } diff --git a/src/Type/Php/RangeFunctionReturnTypeExtension.php b/src/Type/Php/RangeFunctionReturnTypeExtension.php index 1fadd6b2df..1f52b71b3a 100644 --- a/src/Type/Php/RangeFunctionReturnTypeExtension.php +++ b/src/Type/Php/RangeFunctionReturnTypeExtension.php @@ -13,7 +13,10 @@ use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\FloatType; +use PHPStan\Type\GeneralizePrecision; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; @@ -21,8 +24,10 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; +use function count; +use function range; -class RangeFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class RangeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { private const RANGE_LENGTH_THRESHOLD = 50; @@ -34,13 +39,13 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - if (count($functionCall->args) < 2) { + if (count($functionCall->getArgs()) < 2) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } - $startType = $scope->getType($functionCall->args[0]->value); - $endType = $scope->getType($functionCall->args[1]->value); - $stepType = count($functionCall->args) >= 3 ? $scope->getType($functionCall->args[2]->value) : new ConstantIntegerType(1); + $startType = $scope->getType($functionCall->getArgs()[0]->value); + $endType = $scope->getType($functionCall->getArgs()[1]->value); + $stepType = count($functionCall->getArgs()) >= 3 ? $scope->getType($functionCall->getArgs()[2]->value) : new ConstantIntegerType(1); $constantReturnTypes = []; @@ -64,13 +69,28 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $rangeValues = range($startConstant->getValue(), $endConstant->getValue(), $stepConstant->getValue()); if (count($rangeValues) > self::RANGE_LENGTH_THRESHOLD) { + if ($startConstant instanceof ConstantIntegerType && $endConstant instanceof ConstantIntegerType) { + if ($startConstant->getValue() > $endConstant->getValue()) { + $tmp = $startConstant; + $startConstant = $endConstant; + $endConstant = $tmp; + } + return new IntersectionType([ + new ArrayType( + new IntegerType(), + IntegerRangeType::fromInterval($startConstant->getValue(), $endConstant->getValue()), + ), + new NonEmptyArrayType(), + ]); + } + return new IntersectionType([ new ArrayType( new IntegerType(), TypeCombinator::union( - $startConstant->generalize(), - $endConstant->generalize() - ) + $startConstant->generalize(GeneralizePrecision::moreSpecific()), + $endConstant->generalize(GeneralizePrecision::moreSpecific()), + ), ), new NonEmptyArrayType(), ]); @@ -104,7 +124,8 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $numberType = new UnionType([new IntegerType(), new FloatType()]); $isNumber = $numberType->isSuperTypeOf($argType)->yes(); - if ($isNumber) { + $isNumericString = $argType->isNumericString()->yes(); + if ($isNumber || $isNumericString) { return new ArrayType(new IntegerType(), $numberType); } diff --git a/src/Type/Php/ReflectionClassConstructorThrowTypeExtension.php b/src/Type/Php/ReflectionClassConstructorThrowTypeExtension.php index 46a1be0979..3efa672a99 100644 --- a/src/Type/Php/ReflectionClassConstructorThrowTypeExtension.php +++ b/src/Type/Php/ReflectionClassConstructorThrowTypeExtension.php @@ -14,15 +14,13 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeUtils; use ReflectionClass; +use function count; class ReflectionClassConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { - private ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct(private ReflectionProvider $reflectionProvider) { - $this->reflectionProvider = $reflectionProvider; } public function isStaticMethodSupported(MethodReflection $methodReflection): bool @@ -32,11 +30,11 @@ public function isStaticMethodSupported(MethodReflection $methodReflection): boo public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type { - if (count($methodCall->args) < 1) { + if (count($methodCall->getArgs()) < 1) { return $methodReflection->getThrowType(); } - $valueType = $scope->getType($methodCall->args[0]->value); + $valueType = $scope->getType($methodCall->getArgs()[0]->value); foreach (TypeUtils::flattenTypes($valueType) as $type) { if ($type instanceof ClassStringType || $type instanceof ObjectWithoutClassType || $type instanceof ObjectType) { continue; diff --git a/src/Type/Php/ReflectionClassIsSubclassOfTypeSpecifyingExtension.php b/src/Type/Php/ReflectionClassIsSubclassOfTypeSpecifyingExtension.php index 54f9eacab4..842821b1e2 100644 --- a/src/Type/Php/ReflectionClassIsSubclassOfTypeSpecifyingExtension.php +++ b/src/Type/Php/ReflectionClassIsSubclassOfTypeSpecifyingExtension.php @@ -13,6 +13,7 @@ use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\MethodTypeSpecifyingExtension; use PHPStan\Type\ObjectType; +use ReflectionClass; class ReflectionClassIsSubclassOfTypeSpecifyingExtension implements MethodTypeSpecifyingExtension, TypeSpecifierAwareExtension { @@ -26,31 +27,31 @@ public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void public function getClass(): string { - return \ReflectionClass::class; + return ReflectionClass::class; } public function isMethodSupported(MethodReflection $methodReflection, MethodCall $node, TypeSpecifierContext $context): bool { return $methodReflection->getName() === 'isSubclassOf' - && isset($node->args[0]) + && isset($node->getArgs()[0]) && $context->true(); } public function specifyTypes(MethodReflection $methodReflection, MethodCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - $valueType = $scope->getType($node->args[0]->value); + $valueType = $scope->getType($node->getArgs()[0]->value); if (!$valueType instanceof ConstantStringType) { return new SpecifiedTypes([], []); } return $this->typeSpecifier->create( $node->var, - new GenericObjectType(\ReflectionClass::class, [ + new GenericObjectType(ReflectionClass::class, [ new ObjectType($valueType->getValue()), ]), $context, false, - $scope + $scope, ); } diff --git a/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php b/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php index d1232b5043..7dd3ba3e22 100644 --- a/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php +++ b/src/Type/Php/ReflectionFunctionConstructorThrowTypeExtension.php @@ -13,15 +13,13 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use ReflectionFunction; +use function count; class ReflectionFunctionConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { - private ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct(private ReflectionProvider $reflectionProvider) { - $this->reflectionProvider = $reflectionProvider; } public function isStaticMethodSupported(MethodReflection $methodReflection): bool @@ -31,11 +29,11 @@ public function isStaticMethodSupported(MethodReflection $methodReflection): boo public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type { - if (count($methodCall->args) < 1) { + if (count($methodCall->getArgs()) < 1) { return $methodReflection->getThrowType(); } - $valueType = $scope->getType($methodCall->args[0]->value); + $valueType = $scope->getType($methodCall->getArgs()[0]->value); foreach (TypeUtils::getConstantStrings($valueType) as $constantString) { if (!$this->reflectionProvider->hasFunction(new Name($constantString->getValue()), $scope)) { return $methodReflection->getThrowType(); diff --git a/src/Type/Php/ReflectionGetAttributesMethodReturnTypeExtension.php b/src/Type/Php/ReflectionGetAttributesMethodReturnTypeExtension.php new file mode 100644 index 0000000000..6b4a470920 --- /dev/null +++ b/src/Type/Php/ReflectionGetAttributesMethodReturnTypeExtension.php @@ -0,0 +1,67 @@ +className; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'getAttributes'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + if (count($methodCall->getArgs()) === 0) { + return $this->getDefaultReturnType($scope, $methodCall, $methodReflection); + } + $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 $this->getDefaultReturnType($scope, $methodCall, $methodReflection); + } + + return new ArrayType(new MixedType(), new GenericObjectType(ReflectionAttribute::class, [$classType])); + } + + private function getDefaultReturnType(Scope $scope, MethodCall $methodCall, MethodReflection $methodReflection): Type + { + return ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + )->getReturnType(); + } + +} diff --git a/src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php b/src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php index 4d5d3e13f1..18890c4236 100644 --- a/src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php +++ b/src/Type/Php/ReflectionMethodConstructorThrowTypeExtension.php @@ -14,15 +14,13 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use ReflectionMethod; +use function count; class ReflectionMethodConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { - private ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct(private ReflectionProvider $reflectionProvider) { - $this->reflectionProvider = $reflectionProvider; } public function isStaticMethodSupported(MethodReflection $methodReflection): bool @@ -32,12 +30,12 @@ public function isStaticMethodSupported(MethodReflection $methodReflection): boo public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type { - if (count($methodCall->args) < 2) { + if (count($methodCall->getArgs()) < 2) { return $methodReflection->getThrowType(); } - $valueType = $scope->getType($methodCall->args[0]->value); - $propertyType = $scope->getType($methodCall->args[1]->value); + $valueType = $scope->getType($methodCall->getArgs()[0]->value); + $propertyType = $scope->getType($methodCall->getArgs()[1]->value); foreach (TypeUtils::flattenTypes($valueType) as $type) { if ($type instanceof GenericClassStringType) { $classes = $type->getReferencedClasses(); diff --git a/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php b/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php index 4faa7ec8f7..eed7c7d115 100644 --- a/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php +++ b/src/Type/Php/ReflectionPropertyConstructorThrowTypeExtension.php @@ -12,15 +12,13 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use ReflectionProperty; +use function count; class ReflectionPropertyConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { - private ReflectionProvider $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct(private ReflectionProvider $reflectionProvider) { - $this->reflectionProvider = $reflectionProvider; } public function isStaticMethodSupported(MethodReflection $methodReflection): bool @@ -30,12 +28,12 @@ public function isStaticMethodSupported(MethodReflection $methodReflection): boo public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type { - if (count($methodCall->args) < 2) { + if (count($methodCall->getArgs()) < 2) { return $methodReflection->getThrowType(); } - $valueType = $scope->getType($methodCall->args[0]->value); - $propertyType = $scope->getType($methodCall->args[1]->value); + $valueType = $scope->getType($methodCall->getArgs()[0]->value); + $propertyType = $scope->getType($methodCall->getArgs()[1]->value); foreach (TypeUtils::getConstantStrings($valueType) as $constantString) { if (!$this->reflectionProvider->hasClass($constantString->getValue())) { return $methodReflection->getThrowType(); diff --git a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php index 752aa8560f..87bb1aaafb 100644 --- a/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ReplaceFunctionsDynamicReturnTypeExtension.php @@ -6,19 +6,23 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\ArrayType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function array_key_exists; +use function count; class ReplaceFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { /** @var array */ - private array $functions = [ + private array $functionsSubjectPosition = [ 'preg_replace' => 2, 'preg_replace_callback' => 2, 'preg_replace_callback_array' => 1, @@ -27,15 +31,23 @@ class ReplaceFunctionsDynamicReturnTypeExtension implements DynamicFunctionRetur 'substr_replace' => 0, ]; + /** @var array */ + private array $functionsReplacePosition = [ + 'preg_replace' => 1, + 'str_replace' => 1, + 'str_ireplace' => 1, + 'substr_replace' => 1, + ]; + public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return array_key_exists($functionReflection->getName(), $this->functions); + return array_key_exists($functionReflection->getName(), $this->functionsSubjectPosition); } public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope + Scope $scope, ): Type { $type = $this->getPreliminarilyResolvedTypeFromFunctionCall($functionReflection, $functionCall, $scope); @@ -52,19 +64,32 @@ public function getTypeFromFunctionCall( private function getPreliminarilyResolvedTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope + Scope $scope, ): Type { - $argumentPosition = $this->functions[$functionReflection->getName()]; - if (count($functionCall->args) <= $argumentPosition) { + $argumentPosition = $this->functionsSubjectPosition[$functionReflection->getName()]; + if (count($functionCall->getArgs()) <= $argumentPosition) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } - $subjectArgumentType = $scope->getType($functionCall->args[$argumentPosition]->value); + $subjectArgumentType = $scope->getType($functionCall->getArgs()[$argumentPosition]->value); $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); if ($subjectArgumentType instanceof MixedType) { return TypeUtils::toBenevolentUnion($defaultReturnType); } + + if ($subjectArgumentType->isNonEmptyString()->yes() && array_key_exists($functionReflection->getName(), $this->functionsReplacePosition)) { + $replaceArgumentPosition = $this->functionsReplacePosition[$functionReflection->getName()]; + + if (count($functionCall->getArgs()) > $replaceArgumentPosition) { + $replaceArgumentType = $scope->getType($functionCall->getArgs()[$replaceArgumentPosition]->value); + + if ($replaceArgumentType->isNonEmptyString()->yes()) { + return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]); + } + } + } + $stringType = new StringType(); $arrayType = new ArrayType(new MixedType(), new MixedType()); diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..839c5c1587 --- /dev/null +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -0,0 +1,86 @@ +getName(), + [ + 'round', + 'ceil', + 'floor', + ], + true, + ); + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + { + 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) { + return $noArgsReturnType; + } + + $firstArgType = $scope->getType($functionCall->getArgs()[0]->value); + + if ($firstArgType instanceof MixedType) { + return $defaultReturnType; + } + + if ($this->phpVersion->hasStricterRoundFunctions()) { + $allowed = TypeCombinator::union( + new IntegerType(), + new FloatType(), + ); + if ($allowed->isSuperTypeOf($firstArgType)->no()) { + // PHP 8 fatals if the parameter is not an integer or float. + return new NeverType(true); + } + } elseif ($firstArgType->isArray()->yes()) { + // PHP 7 returns false if the parameter is an array. + return new ConstantBooleanType(false); + } + + return new FloatType(); + } + +} diff --git a/src/Type/Php/SimpleXMLElementAsXMLMethodReturnTypeExtension.php b/src/Type/Php/SimpleXMLElementAsXMLMethodReturnTypeExtension.php index 562ec373be..e6c390cd22 100644 --- a/src/Type/Php/SimpleXMLElementAsXMLMethodReturnTypeExtension.php +++ b/src/Type/Php/SimpleXMLElementAsXMLMethodReturnTypeExtension.php @@ -12,6 +12,7 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; use SimpleXMLElement; +use function count; class SimpleXMLElementAsXMLMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension { @@ -28,7 +29,7 @@ public function isMethodSupported(MethodReflection $methodReflection): bool public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type { - if (count($methodCall->args) === 1) { + if (count($methodCall->getArgs()) === 1) { return new BooleanType(); } return new UnionType([new StringType(), new ConstantBooleanType(false)]); diff --git a/src/Type/Php/SimpleXMLElementClassPropertyReflectionExtension.php b/src/Type/Php/SimpleXMLElementClassPropertyReflectionExtension.php index 9646898c00..503e998fd3 100644 --- a/src/Type/Php/SimpleXMLElementClassPropertyReflectionExtension.php +++ b/src/Type/Php/SimpleXMLElementClassPropertyReflectionExtension.php @@ -6,6 +6,8 @@ use PHPStan\Reflection\Php\SimpleXMLElementProperty; use PHPStan\Reflection\PropertiesClassReflectionExtension; use PHPStan\Reflection\PropertyReflection; +use PHPStan\Type\BenevolentUnionType; +use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; class SimpleXMLElementClassPropertyReflectionExtension implements PropertiesClassReflectionExtension @@ -19,7 +21,7 @@ public function hasProperty(ClassReflection $classReflection, string $propertyNa public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection { - return new SimpleXMLElementProperty($classReflection, new ObjectType($classReflection->getName())); + return new SimpleXMLElementProperty($classReflection, new BenevolentUnionType([new ObjectType($classReflection->getName()), new NullType()])); } } diff --git a/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php b/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php index ec572c38fe..e7b8247870 100644 --- a/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php +++ b/src/Type/Php/SimpleXMLElementConstructorThrowTypeExtension.php @@ -11,6 +11,7 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use SimpleXMLElement; +use function count; class SimpleXMLElementConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension { @@ -22,11 +23,11 @@ public function isStaticMethodSupported(MethodReflection $methodReflection): boo public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type { - if (count($methodCall->args) === 0) { + if (count($methodCall->getArgs()) === 0) { return $methodReflection->getThrowType(); } - $valueType = $scope->getType($methodCall->args[0]->value); + $valueType = $scope->getType($methodCall->getArgs()[0]->value); $constantStrings = TypeUtils::getConstantStrings($valueType); foreach ($constantStrings as $constantString) { diff --git a/src/Type/Php/SimpleXMLElementXpathMethodReturnTypeExtension.php b/src/Type/Php/SimpleXMLElementXpathMethodReturnTypeExtension.php index 06da90da2a..175cd703ed 100644 --- a/src/Type/Php/SimpleXMLElementXpathMethodReturnTypeExtension.php +++ b/src/Type/Php/SimpleXMLElementXpathMethodReturnTypeExtension.php @@ -7,18 +7,20 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\ArrayType; +use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use SimpleXMLElement; -class SimpleXMLElementXpathMethodReturnTypeExtension implements \PHPStan\Type\DynamicMethodReturnTypeExtension +class SimpleXMLElementXpathMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension { public function getClass(): string { - return \SimpleXMLElement::class; + return SimpleXMLElement::class; } public function isMethodSupported(MethodReflection $methodReflection): bool @@ -28,13 +30,13 @@ public function isMethodSupported(MethodReflection $methodReflection): bool public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type { - if (!isset($methodCall->args[0])) { + if (!isset($methodCall->getArgs()[0])) { return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); } - $argType = $scope->getType($methodCall->args[0]->value); + $argType = $scope->getType($methodCall->getArgs()[0]->value); - $xmlElement = new \SimpleXMLElement(''); + $xmlElement = new SimpleXMLElement(''); foreach (TypeUtils::getConstantStrings($argType) as $constantString) { $result = @$xmlElement->xpath($constantString->getValue()); diff --git a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php index 62016da18f..b3736e663a 100644 --- a/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/SprintfFunctionDynamicReturnTypeExtension.php @@ -6,10 +6,17 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\ConstantScalarType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use Throwable; +use function array_shift; +use function count; +use function is_string; +use function sprintf; class SprintfFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -22,12 +29,26 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope + Scope $scope, ): Type { + $args = $functionCall->getArgs(); + if (count($args) === 0) { + return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + } + + $formatType = $scope->getType($args[0]->value); + if ($formatType->isNonEmptyString()->yes()) { + $returnType = new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } else { + $returnType = new StringType(); + } + $values = []; - $returnType = new StringType(); - foreach ($functionCall->args as $arg) { + foreach ($args as $arg) { $argType = $scope->getType($arg->value); if (!$argType instanceof ConstantScalarType) { return $returnType; @@ -36,18 +57,14 @@ public function getTypeFromFunctionCall( $values[] = $argType->getValue(); } - if (count($values) === 0) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - } - $format = array_shift($values); if (!is_string($format)) { - return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + return $returnType; } try { $value = @sprintf($format, ...$values); - } catch (\Throwable $e) { + } catch (Throwable) { return $returnType; } diff --git a/src/Type/Php/StatDynamicReturnTypeExtension.php b/src/Type/Php/StatDynamicReturnTypeExtension.php index ccc97ff5c9..7b8c08f194 100644 --- a/src/Type/Php/StatDynamicReturnTypeExtension.php +++ b/src/Type/Php/StatDynamicReturnTypeExtension.php @@ -10,11 +10,15 @@ use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\IntegerType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use SplFileObject; +use function in_array; -class StatDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension, \PHPStan\Type\DynamicMethodReturnTypeExtension +class StatDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension, DynamicMethodReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -24,12 +28,12 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - return $this->getReturnType(); + return TypeCombinator::union($this->getReturnType(), new ConstantBooleanType(false)); } public function getClass(): string { - return \SplFileObject::class; + return SplFileObject::class; } public function isMethodSupported(MethodReflection $methodReflection): bool @@ -70,7 +74,7 @@ private function getReturnType(): Type $builder->setOffsetValueType(new ConstantStringType($key), $valueType); } - return TypeCombinator::union($builder->getArray(), new ConstantBooleanType(false)); + return $builder->getArray(); } } diff --git a/src/Type/Php/StrPadFunctionReturnTypeExtension.php b/src/Type/Php/StrPadFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..3c7f831331 --- /dev/null +++ b/src/Type/Php/StrPadFunctionReturnTypeExtension.php @@ -0,0 +1,63 @@ +getName() === 'str_pad'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): Type + { + $args = $functionCall->getArgs(); + if (count($args) < 2) { + return new StringType(); + } + + $inputType = $scope->getType($args[0]->value); + $lengthType = $scope->getType($args[1]->value); + + $accessoryTypes = []; + if ($inputType->isNonEmptyString()->yes() || IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($lengthType)->yes()) { + $accessoryTypes[] = new AccessoryNonEmptyStringType(); + } + + if ($inputType->isLiteralString()->yes()) { + if (count($args) < 3) { + $accessoryTypes[] = new AccessoryLiteralStringType(); + } else { + $padStringType = $scope->getType($args[2]->value); + if ($padStringType->isLiteralString()->yes()) { + $accessoryTypes[] = new AccessoryLiteralStringType(); + } + } + } + + if (count($accessoryTypes) > 0) { + $accessoryTypes[] = new StringType(); + return new IntersectionType($accessoryTypes); + } + + return new StringType(); + } + +} diff --git a/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php b/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..cc8a09a3cd --- /dev/null +++ b/src/Type/Php/StrRepeatFunctionReturnTypeExtension.php @@ -0,0 +1,74 @@ +getName() === 'str_repeat'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): Type + { + $args = $functionCall->getArgs(); + if (count($args) < 2) { + return new StringType(); + } + + $inputType = $scope->getType($args[0]->value); + $multiplierType = $scope->getType($args[1]->value); + + if ((new ConstantIntegerType(0))->isSuperTypeOf($multiplierType)->yes()) { + return new ConstantStringType(''); + } + + if ($multiplierType instanceof ConstantIntegerType && $multiplierType->getValue() < 0) { + return new NeverType(); + } + + if ($inputType instanceof ConstantStringType && $multiplierType instanceof ConstantIntegerType) { + return new ConstantStringType(str_repeat($inputType->getValue(), $multiplierType->getValue())); + } + + $accessoryTypes = []; + if ($inputType->isNonEmptyString()->yes()) { + if (IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($multiplierType)->yes()) { + $accessoryTypes[] = new AccessoryNonEmptyStringType(); + } + } + + if ($inputType->isLiteralString()->yes()) { + $accessoryTypes[] = new AccessoryLiteralStringType(); + } + + if (count($accessoryTypes) > 0) { + $accessoryTypes[] = new StringType(); + return new IntersectionType($accessoryTypes); + } + + return new StringType(); + } + +} diff --git a/src/Type/Php/StrSplitFunctionReturnTypeExtension.php b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php index 0157f68db2..dce61f03fd 100644 --- a/src/Type/Php/StrSplitFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrSplitFunctionReturnTypeExtension.php @@ -6,6 +6,8 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\ShouldNotHappenException; +use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -15,7 +17,20 @@ use PHPStan\Type\IntegerType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function array_map; +use function array_merge; +use function array_unique; +use function count; +use function function_exists; +use function in_array; +use function mb_encoding_aliases; +use function mb_internal_encoding; +use function mb_list_encodings; +use function mb_str_split; +use function str_split; +use function strtoupper; final class StrSplitFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -30,7 +45,7 @@ public function __construct() foreach (mb_list_encodings() as $encoding) { $aliases = mb_encoding_aliases($encoding); if ($aliases === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $supportedEncodings = array_merge($supportedEncodings, $aliases, [$encoding]); } @@ -47,12 +62,12 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, { $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - if (count($functionCall->args) < 1) { + if (count($functionCall->getArgs()) < 1) { return $defaultReturnType; } - if (count($functionCall->args) >= 2) { - $splitLengthType = $scope->getType($functionCall->args[1]->value); + if (count($functionCall->getArgs()) >= 2) { + $splitLengthType = $scope->getType($functionCall->getArgs()[1]->value); if ($splitLengthType instanceof ConstantIntegerType) { $splitLength = $splitLengthType->getValue(); if ($splitLength < 1) { @@ -64,11 +79,9 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if ($functionReflection->getName() === 'mb_str_split') { - if (count($functionCall->args) >= 3) { - $strings = TypeUtils::getConstantStrings($scope->getType($functionCall->args[2]->value)); - $values = array_unique(array_map(static function (ConstantStringType $encoding): string { - return $encoding->getValue(); - }, $strings)); + if (count($functionCall->getArgs()) >= 3) { + $strings = TypeUtils::getConstantStrings($scope->getType($functionCall->getArgs()[2]->value)); + $values = array_unique(array_map(static fn (ConstantStringType $encoding): string => $encoding->getValue(), $strings)); if (count($values) !== 1) { return $defaultReturnType; @@ -87,17 +100,20 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $defaultReturnType; } - $stringType = $scope->getType($functionCall->args[0]->value); + $stringType = $scope->getType($functionCall->getArgs()[0]->value); if (!$stringType instanceof ConstantStringType) { - return new ArrayType(new IntegerType(), new StringType()); + return TypeCombinator::intersect( + new ArrayType(new IntegerType(), new StringType()), + new NonEmptyArrayType(), + ); } $stringValue = $stringType->getValue(); $items = isset($encoding) ? mb_str_split($stringValue, $splitLength, $encoding) : str_split($stringValue, $splitLength); - if (!is_array($items)) { - throw new \PHPStan\ShouldNotHappenException(); + if ($items === false) { + throw new ShouldNotHappenException(); } return self::createConstantArrayFrom($items, $scope); @@ -110,8 +126,6 @@ private function isSupportedEncoding(string $encoding): bool /** * @param string[] $constantArray - * @param \PHPStan\Analyser\Scope $scope - * @return \PHPStan\Type\Constant\ConstantArrayType */ private static function createConstantArrayFrom(array $constantArray, Scope $scope): ConstantArrayType { @@ -123,7 +137,7 @@ private static function createConstantArrayFrom(array $constantArray, Scope $sco foreach ($constantArray as $key => $value) { $keyType = $scope->getTypeFromValue($key); if (!$keyType instanceof ConstantIntegerType) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $keyTypes[] = $keyType; diff --git a/src/Type/Php/StrTokFunctionReturnTypeExtension.php b/src/Type/Php/StrTokFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..0d82a17a37 --- /dev/null +++ b/src/Type/Php/StrTokFunctionReturnTypeExtension.php @@ -0,0 +1,44 @@ +getName() === 'strtok'; + } + + 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(); + } + + $delimiterType = $scope->getType($functionCall->getArgs()[0]->value); + $isEmptyString = (new ConstantStringType(''))->isSuperTypeOf($delimiterType); + if ($isEmptyString->yes()) { + return new ConstantBooleanType(false); + } + + if ($isEmptyString->no()) { + return new StringType(); + } + + return ParametersAcceptorSelector::selectFromArgs($scope, $args, $functionReflection->getVariants())->getReturnType(); + } + +} diff --git a/src/Type/Php/StrWordCountFunctionDynamicReturnTypeExtension.php b/src/Type/Php/StrWordCountFunctionDynamicReturnTypeExtension.php index 6e53059859..96a532ceef 100644 --- a/src/Type/Php/StrWordCountFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/StrWordCountFunctionDynamicReturnTypeExtension.php @@ -2,18 +2,21 @@ namespace PHPStan\Type\Php; +use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\ErrorType; use PHPStan\Type\IntegerType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; +use function count; -class StrWordCountFunctionDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class StrWordCountFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -23,15 +26,15 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, - \PhpParser\Node\Expr\FuncCall $functionCall, - Scope $scope + Node\Expr\FuncCall $functionCall, + Scope $scope, ): Type { - $argsCount = count($functionCall->args); + $argsCount = count($functionCall->getArgs()); if ($argsCount === 1) { return new IntegerType(); } elseif ($argsCount === 2 || $argsCount === 3) { - $formatType = $scope->getType($functionCall->args[1]->value); + $formatType = $scope->getType($functionCall->getArgs()[1]->value); if ($formatType instanceof ConstantIntegerType) { $val = $formatType->getValue(); if ($val === 0) { diff --git a/src/Type/Php/StrlenFunctionReturnTypeExtension.php b/src/Type/Php/StrlenFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..e06ccc1e52 --- /dev/null +++ b/src/Type/Php/StrlenFunctionReturnTypeExtension.php @@ -0,0 +1,84 @@ +getName() === 'strlen'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): Type + { + $args = $functionCall->getArgs(); + if (count($args) === 0) { + return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + } + + $argType = $scope->getType($args[0]->value); + + $constantStrings = TypeUtils::getConstantStrings($argType); + $min = null; + $max = null; + foreach ($constantStrings as $constantString) { + $len = strlen($constantString->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); + } + + $isNonEmpty = $argType->isNonEmptyString(); + $integer = new IntegerType(); + if ($isNonEmpty->yes() || $integer->isSuperTypeOf($argType)->yes()) { + return IntegerRangeType::fromInterval(1, null); + } + + if ($isNonEmpty->no()) { + return new ConstantIntegerType(0); + } + + return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + } + +} diff --git a/src/Type/Php/StrtotimeFunctionReturnTypeExtension.php b/src/Type/Php/StrtotimeFunctionReturnTypeExtension.php index 767c141307..304b5401d3 100644 --- a/src/Type/Php/StrtotimeFunctionReturnTypeExtension.php +++ b/src/Type/Php/StrtotimeFunctionReturnTypeExtension.php @@ -8,12 +8,20 @@ use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\TypeUtils; +use function array_map; +use function array_unique; +use function count; +use function gettype; +use function min; +use function strtotime; -class StrtotimeFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class StrtotimeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -24,22 +32,35 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); - if (count($functionCall->args) === 0) { + if (count($functionCall->getArgs()) === 0) { return $defaultReturnType; } - $argType = $scope->getType($functionCall->args[0]->value); + $argType = $scope->getType($functionCall->getArgs()[0]->value); if ($argType instanceof MixedType) { return TypeUtils::toBenevolentUnion($defaultReturnType); } - $result = array_unique(array_map(static function (ConstantStringType $string): bool { - return is_int(strtotime($string->getValue())); - }, TypeUtils::getConstantStrings($argType))); + $results = array_unique(array_map(static fn (ConstantStringType $string): int|bool => strtotime($string->getValue()), TypeUtils::getConstantStrings($argType))); + $resultTypes = array_unique(array_map(static fn (int|bool $value): string => gettype($value), $results)); - if (count($result) !== 1) { + if (count($resultTypes) !== 1 || count($results) === 0) { return $defaultReturnType; } - return $result[0] ? new IntegerType() : new ConstantBooleanType(false); + if ($results[0] === false) { + return new ConstantBooleanType(false); + } + + // 2nd param $baseTimestamp is too non-deterministic so simply return int + if (count($functionCall->getArgs()) > 1) { + return new IntegerType(); + } + + // if it is positive we can narrow down to positive-int as long as time flows forward + if (min(array_map('intval', $results)) > 0) { + return IntegerRangeType::createAllGreaterThan(0); + } + + return new IntegerType(); } } diff --git a/src/Type/Php/StrvalFamilyFunctionReturnTypeExtension.php b/src/Type/Php/StrvalFamilyFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..cef472cf06 --- /dev/null +++ b/src/Type/Php/StrvalFamilyFunctionReturnTypeExtension.php @@ -0,0 +1,58 @@ +getName(), self::FUNCTIONS, true); + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): Type + { + if (count($functionCall->getArgs()) === 0) { + return new NullType(); + } + + $argType = $scope->getType($functionCall->getArgs()[0]->value); + + switch ($functionReflection->getName()) { + case 'strval': + return $argType->toString(); + case 'intval': + return $argType->toInteger(); + case 'boolval': + return $argType->toBoolean(); + case 'floatval': + case 'doubleval': + return $argType->toFloat(); + default: + throw new ShouldNotHappenException(); + } + } + +} diff --git a/src/Type/Php/SubstrDynamicReturnTypeExtension.php b/src/Type/Php/SubstrDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..3aa09682cc --- /dev/null +++ b/src/Type/Php/SubstrDynamicReturnTypeExtension.php @@ -0,0 +1,61 @@ +getName() === 'substr'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): Type + { + $args = $functionCall->getArgs(); + if (count($args) === 0) { + return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + } + + if (count($args) >= 2) { + $string = $scope->getType($args[0]->value); + $offset = $scope->getType($args[1]->value); + + $negativeOffset = IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($offset)->yes(); + $zeroOffset = (new ConstantIntegerType(0))->isSuperTypeOf($offset)->yes(); + $positiveLength = false; + + if (count($args) === 3) { + $length = $scope->getType($args[2]->value); + $positiveLength = IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($length)->yes(); + } + + if ($string->isNonEmptyString()->yes() && ($negativeOffset || $zeroOffset && $positiveLength)) { + return new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]); + } + } + + return new StringType(); + } + +} diff --git a/src/Type/Php/ThrowableReturnTypeExtension.php b/src/Type/Php/ThrowableReturnTypeExtension.php new file mode 100644 index 0000000000..fa3a1a8d3b --- /dev/null +++ b/src/Type/Php/ThrowableReturnTypeExtension.php @@ -0,0 +1,78 @@ +getName() === 'getCode'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + $type = $scope->getType($methodCall->var); + $types = []; + $pdoException = new ObjectType('PDOException'); + foreach (TypeUtils::getDirectClassNames($type) as $class) { + $classType = new ObjectType($class); + if ($classType->getClassReflection() !== null) { + $classReflection = $classType->getClassReflection(); + foreach ($classReflection->getMethodTags() as $methodName => $methodTag) { + if (strtolower($methodName) !== 'getcode') { + continue; + } + + $types[] = $methodTag->getReturnType(); + continue 2; + } + } + + if ($pdoException->isSuperTypeOf($classType)->yes()) { + $types[] = new BenevolentUnionType([new IntegerType(), new StringType()]); + continue; + } + + if (in_array(strtolower($class), [ + 'throwable', + 'exception', + 'runtimeexception', + ], true)) { + $types[] = new BenevolentUnionType([new IntegerType(), new StringType()]); + continue; + } + + $types[] = new IntegerType(); + } + + if (count($types) === 0) { + return new ErrorType(); + } + + return TypeCombinator::union(...$types); + } + +} diff --git a/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php b/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..1f7566f28e --- /dev/null +++ b/src/Type/Php/TriggerErrorDynamicReturnTypeExtension.php @@ -0,0 +1,40 @@ +getName() === 'trigger_error'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + { + if (count($functionCall->getArgs()) < 2) { + return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + } + + $errorType = $scope->getType($functionCall->getArgs()[1]->value); + if ($errorType instanceof ConstantScalarType) { + if ($errorType->getValue() === E_USER_ERROR) { + return new NeverType(true); + } + } + + return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + } + +} diff --git a/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php index 9dc7d71a13..08eaf84e5b 100644 --- a/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php @@ -6,34 +6,28 @@ use PHPStan\Analyser\Scope; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; -use PHPStan\Broker\Broker; -use PHPStan\Reflection\BrokerAwareExtension; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; +use function count; +use function in_array; -class TypeSpecifyingFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension, TypeSpecifierAwareExtension, BrokerAwareExtension +class TypeSpecifyingFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension, TypeSpecifierAwareExtension { - private bool $treatPhpDocTypesAsCertain; + private TypeSpecifier $typeSpecifier; - private \PHPStan\Broker\Broker $broker; + private ?ImpossibleCheckTypeHelper $helper = null; - private \PHPStan\Analyser\TypeSpecifier $typeSpecifier; - - private ?\PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper $helper = null; - - public function __construct(bool $treatPhpDocTypesAsCertain) - { - $this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain; - } - - public function setBroker(Broker $broker): void + /** + * @param string[] $universalObjectCratesClasses + */ + public function __construct(private ReflectionProvider $reflectionProvider, private bool $treatPhpDocTypesAsCertain, private array $universalObjectCratesClasses) { - $this->broker = $broker; } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void @@ -68,16 +62,16 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope + Scope $scope, ): Type { - if (count($functionCall->args) === 0) { + if (count($functionCall->getArgs()) === 0) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); } $isAlways = $this->getHelper()->findSpecifiedType( $scope, - $functionCall + $functionCall, ); if ($isAlways === null) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); @@ -89,7 +83,7 @@ public function getTypeFromFunctionCall( private function getHelper(): ImpossibleCheckTypeHelper { if ($this->helper === null) { - $this->helper = new ImpossibleCheckTypeHelper($this->broker, $this->typeSpecifier, $this->broker->getUniversalObjectCratesClasses(), $this->treatPhpDocTypesAsCertain); + $this->helper = new ImpossibleCheckTypeHelper($this->reflectionProvider, $this->typeSpecifier, $this->universalObjectCratesClasses, $this->treatPhpDocTypesAsCertain); } return $this->helper; diff --git a/src/Type/Php/VarExportFunctionDynamicReturnTypeExtension.php b/src/Type/Php/VarExportFunctionDynamicReturnTypeExtension.php index b90055cd8d..7a6aa95976 100644 --- a/src/Type/Php/VarExportFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/VarExportFunctionDynamicReturnTypeExtension.php @@ -2,17 +2,23 @@ namespace PHPStan\Type\Php; +use PhpParser\Node; +use PHPStan\Analyser\Scope; +use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NullType; use PHPStan\Type\StringType; +use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function count; +use function in_array; class VarExportFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - public function isFunctionSupported(\PHPStan\Reflection\FunctionReflection $functionReflection): bool + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return in_array( $functionReflection->getName(), @@ -22,11 +28,11 @@ public function isFunctionSupported(\PHPStan\Reflection\FunctionReflection $func 'highlight_string', 'print_r', ], - true + true, ); } - public function getTypeFromFunctionCall(\PHPStan\Reflection\FunctionReflection $functionReflection, \PhpParser\Node\Expr\FuncCall $functionCall, \PHPStan\Analyser\Scope $scope): \PHPStan\Type\Type + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, Node\Expr\FuncCall $functionCall, Scope $scope): Type { if ($functionReflection->getName() === 'var_export') { $fallbackReturnType = new NullType(); @@ -36,18 +42,18 @@ public function getTypeFromFunctionCall(\PHPStan\Reflection\FunctionReflection $ $fallbackReturnType = new BooleanType(); } - if (count($functionCall->args) < 1) { + if (count($functionCall->getArgs()) < 1) { return TypeCombinator::union( new StringType(), - $fallbackReturnType + $fallbackReturnType, ); } - if (count($functionCall->args) < 2) { + if (count($functionCall->getArgs()) < 2) { return $fallbackReturnType; } - $returnArgumentType = $scope->getType($functionCall->args[1]->value); + $returnArgumentType = $scope->getType($functionCall->getArgs()[1]->value); if ((new ConstantBooleanType(true))->isSuperTypeOf($returnArgumentType)->yes()) { return new StringType(); } diff --git a/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php b/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php index d0a6c37763..f695b20899 100644 --- a/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/VersionCompareFunctionDynamicReturnTypeExtension.php @@ -9,11 +9,15 @@ use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use function array_filter; +use function count; +use function version_compare; -class VersionCompareFunctionDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension +class VersionCompareFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { public function isFunctionSupported(FunctionReflection $functionReflection): bool @@ -24,41 +28,37 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, - Scope $scope + Scope $scope, ): Type { - if (count($functionCall->args) < 2) { - return ParametersAcceptorSelector::selectFromArgs($scope, $functionCall->args, $functionReflection->getVariants())->getReturnType(); + if (count($functionCall->getArgs()) < 2) { + return ParametersAcceptorSelector::selectFromArgs($scope, $functionCall->getArgs(), $functionReflection->getVariants())->getReturnType(); } - $version1Strings = TypeUtils::getConstantStrings($scope->getType($functionCall->args[0]->value)); - $version2Strings = TypeUtils::getConstantStrings($scope->getType($functionCall->args[1]->value)); + $version1Strings = TypeUtils::getConstantStrings($scope->getType($functionCall->getArgs()[0]->value)); + $version2Strings = TypeUtils::getConstantStrings($scope->getType($functionCall->getArgs()[1]->value)); $counts = [ count($version1Strings), count($version2Strings), ]; - if (isset($functionCall->args[2])) { - $operatorStrings = TypeUtils::getConstantStrings($scope->getType($functionCall->args[2]->value)); + if (isset($functionCall->getArgs()[2])) { + $operatorStrings = TypeUtils::getConstantStrings($scope->getType($functionCall->getArgs()[2]->value)); $counts[] = count($operatorStrings); $returnType = new BooleanType(); } else { $returnType = TypeCombinator::union( new ConstantIntegerType(-1), new ConstantIntegerType(0), - new ConstantIntegerType(1) + new ConstantIntegerType(1), ); } - if (count(array_filter($counts, static function (int $count): bool { - return $count === 0; - })) > 0) { + if (count(array_filter($counts, static fn (int $count): bool => $count === 0)) > 0) { return $returnType; // one of the arguments is not a constant string } - if (count(array_filter($counts, static function (int $count): bool { - return $count > 1; - })) > 1) { + if (count(array_filter($counts, static fn (int $count): bool => $count > 1)) > 1) { return $returnType; // more than one argument can have multiple possibilities, avoid combinatorial explosion } diff --git a/src/Type/RecursionGuard.php b/src/Type/RecursionGuard.php index 6e1cf7191a..2149fb1015 100644 --- a/src/Type/RecursionGuard.php +++ b/src/Type/RecursionGuard.php @@ -9,10 +9,8 @@ class RecursionGuard private static array $context = []; /** - * @param Type $type * @param callable(): Type $callback * - * @return Type */ public static function run(Type $type, callable $callback): Type { diff --git a/src/Type/ResourceType.php b/src/Type/ResourceType.php index 7cb78a65ad..1f1028b11a 100644 --- a/src/Type/ResourceType.php +++ b/src/Type/ResourceType.php @@ -2,13 +2,15 @@ namespace PHPStan\Type; -use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Traits\NonCallableTypeTrait; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; +use PHPStan\Type\Traits\NonOffsetAccessibleTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\TruthyBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; @@ -23,6 +25,9 @@ class ResourceType implements Type use TruthyBooleanTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonTypeTrait; + use NonOffsetAccessibleTypeTrait; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; /** @api */ public function __construct() @@ -59,33 +64,12 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1 + 1, ); } - public function isOffsetAccessible(): TrinaryLogic - { - return TrinaryLogic::createNo(); - } - - public function hasOffsetValueType(Type $offsetType): TrinaryLogic - { - return TrinaryLogic::createNo(); - } - - public function getOffsetValueType(Type $offsetType): Type - { - return new ErrorType(); - } - - public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type - { - return new ErrorType(); - } - /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index 4fda7a3c7b..e4d2567b3d 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -2,56 +2,57 @@ namespace PHPStan\Type; -use PHPStan\Broker\Broker; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\PropertyReflection; +use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\Reflection\Type\CallbackUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\CallbackUnresolvedPropertyPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; +use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateTypeHelper; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; +use function array_keys; +use function array_values; +use function count; +use function get_class; +use function sprintf; /** @api */ -class StaticType implements TypeWithClassName +class StaticType implements TypeWithClassName, SubtractableType { use NonGenericTypeTrait; use UndecidedComparisonTypeTrait; + use NonGeneralizableTypeTrait; - private ?ClassReflection $classReflection; + private ?Type $subtractedType; - private ?\PHPStan\Type\ObjectType $staticObjectType = null; + private ?ObjectType $staticObjectType = null; private string $baseClass; /** * @api - * @param string|ClassReflection $classReflection */ - public function __construct($classReflection) - { - if (is_string($classReflection)) { - $broker = Broker::getInstance(); - if ($broker->hasClass($classReflection)) { - $classReflection = $broker->getClass($classReflection); - $this->classReflection = $classReflection; - $this->baseClass = $classReflection->getName(); - return; - } - - $this->classReflection = null; - $this->baseClass = $classReflection; - return; + public function __construct( + private ClassReflection $classReflection, + ?Type $subtractedType = null, + ) + { + if ($subtractedType instanceof NeverType) { + $subtractedType = null; } - $this->classReflection = $classReflection; + $this->subtractedType = $subtractedType; $this->baseClass = $classReflection->getName(); } @@ -72,23 +73,27 @@ public function getAncestorWithClassName(string $className): ?TypeWithClassName return null; } - return $this->changeBaseClass($ancestor->getClassReflection() ?? $ancestor->getClassName()); + $classReflection = $ancestor->getClassReflection(); + if ($classReflection !== null) { + return $this->changeBaseClass($classReflection); + } + + return null; } public function getStaticObjectType(): ObjectType { if ($this->staticObjectType === null) { - if ($this->classReflection !== null && $this->classReflection->isGeneric()) { - $typeMap = $this->classReflection->getActiveTemplateTypeMap()->map(static function (string $name, Type $type): Type { - return TemplateTypeHelper::toArgument($type); - }); + if ($this->classReflection->isGeneric()) { + $typeMap = $this->classReflection->getActiveTemplateTypeMap()->map(static fn (string $name, Type $type): Type => TemplateTypeHelper::toArgument($type)); return $this->staticObjectType = new GenericObjectType( $this->classReflection->getName(), - $this->classReflection->typeMapToList($typeMap) + $this->classReflection->typeMapToList($typeMap), + $this->subtractedType, ); } - return $this->staticObjectType = new ObjectType($this->baseClass, null, $this->classReflection); + return $this->staticObjectType = new ObjectType($this->classReflection->getName(), $this->subtractedType, $this->classReflection); } return $this->staticObjectType; @@ -102,15 +107,10 @@ public function getReferencedClasses(): array return $this->getStaticObjectType()->getReferencedClasses(); } - public function getBaseClass(): string - { - return $this->baseClass; - } - public function accepts(Type $type, bool $strictTypes): TrinaryLogic { if ($type instanceof CompoundType) { - return CompoundTypeHelper::accepts($type, $this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } if (!$type instanceof static) { @@ -160,7 +160,7 @@ public function equals(Type $type): bool public function describe(VerbosityLevel $level): string { - return sprintf('static(%s)', $this->getClassName()); + return sprintf('static(%s)', $this->getStaticObjectType()->describe($level)); } public function canAccessProperties(): TrinaryLogic @@ -196,9 +196,7 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember $nakedProperty, $classReflection, false, - function (Type $type) use ($scope): Type { - return $this->transformStaticType($type, $scope); - } + fn (Type $type): Type => $this->transformStaticType($type, $scope), ); } @@ -235,9 +233,7 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce $nakedMethod, $classReflection, false, - function (Type $type) use ($scope): Type { - return $this->transformStaticType($type, $scope); - } + fn (Type $type): Type => $this->transformStaticType($type, $scope), ); } @@ -247,9 +243,7 @@ private function transformStaticType(Type $type, ClassMemberAccessAnswerer $scop if ($type instanceof StaticType) { $classReflection = $this->classReflection; $isFinal = false; - if ($classReflection === null) { - $classReflection = $this->baseClass; - } elseif ($scope->isInClass()) { + if ($scope->isInClass()) { $classReflection = $scope->getClassReflection(); $isFinal = $classReflection->isFinal(); } @@ -280,13 +274,9 @@ public function getConstant(string $constantName): ConstantReflection return $this->getStaticObjectType()->getConstant($constantName); } - /** - * @param ClassReflection|string $classReflection - * @return self - */ - public function changeBaseClass($classReflection): self + public function changeBaseClass(ClassReflection $classReflection): self { - return new self($classReflection); + return new self($classReflection, $this->subtractedType); } public function isIterable(): TrinaryLogic @@ -329,6 +319,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this->getStaticObjectType()->setOffsetValueType($offsetType, $valueType, $unionValues); } + public function unsetOffset(Type $offsetType): Type + { + return $this->getStaticObjectType()->unsetOffset($offsetType); + } + public function isCallable(): TrinaryLogic { return $this->getStaticObjectType()->isCallable(); @@ -339,14 +334,28 @@ public function isArray(): TrinaryLogic return $this->getStaticObjectType()->isArray(); } + public function isString(): TrinaryLogic + { + return $this->getStaticObjectType()->isString(); + } + public function isNumericString(): TrinaryLogic { return $this->getStaticObjectType()->isNumericString(); } + public function isNonEmptyString(): TrinaryLogic + { + return $this->getStaticObjectType()->isNonEmptyString(); + } + + public function isLiteralString(): TrinaryLogic + { + return $this->getStaticObjectType()->isLiteralString(); + } + /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] + * @return ParametersAcceptor[] */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { @@ -393,13 +402,81 @@ public function traverse(callable $cb): Type return $this; } + public function subtract(Type $type): Type + { + if ($this->subtractedType !== null) { + $type = TypeCombinator::union($this->subtractedType, $type); + } + + return $this->changeSubtractedType($type); + } + + public function getTypeWithoutSubtractedType(): Type + { + return $this->changeSubtractedType(null); + } + + public function changeSubtractedType(?Type $subtractedType): Type + { + $classReflection = $this->getClassReflection(); + if ($classReflection !== null && $classReflection->isEnum() && $subtractedType !== null) { + $cases = []; + foreach (array_keys($classReflection->getEnumCases()) as $constantName) { + $cases[$constantName] = new EnumCaseObjectType($classReflection->getName(), $constantName); + } + + foreach (TypeUtils::flattenTypes($subtractedType) as $subType) { + if (!$subType instanceof EnumCaseObjectType) { + return new self($this->classReflection, $subtractedType); + } + + if ($subType->getClassName() !== $this->getClassName()) { + return new self($this->classReflection, $subtractedType); + } + + unset($cases[$subType->getEnumCaseName()]); + } + + $cases = array_values($cases); + if (count($cases) === 0) { + return new NeverType(); + } + + if (count($cases) === 1) { + return $cases[0]; + } + + return new UnionType(array_values($cases)); + } + + return new self($this->classReflection, $subtractedType); + } + + public function getSubtractedType(): ?Type + { + return $this->subtractedType; + } + + public function tryRemove(Type $typeToRemove): ?Type + { + if ($this->isSuperTypeOf($typeToRemove)->yes()) { + return $this->subtract($typeToRemove); + } + + return null; + } + /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { - return new self($properties['baseClass']); + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + if ($reflectionProvider->hasClass($properties['baseClass'])) { + return new self($reflectionProvider->getClass($properties['baseClass']), $properties['subtractedType'] ?? null); + } + + return new ErrorType(); } } diff --git a/src/Type/StrictMixedType.php b/src/Type/StrictMixedType.php index 21f3d01cc6..1250c77a5e 100644 --- a/src/Type/StrictMixedType.php +++ b/src/Type/StrictMixedType.php @@ -8,16 +8,21 @@ use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateMixedType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait; class StrictMixedType implements CompoundType { use UndecidedComparisonCompoundTypeTrait; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; public function getReferencedClasses(): array { @@ -26,24 +31,36 @@ public function getReferencedClasses(): array public function accepts(Type $type, bool $strictTypes): TrinaryLogic { - return TrinaryLogic::createNo(); + return TrinaryLogic::createYes(); } public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic { - return TrinaryLogic::createFromBoolean( - $acceptingType instanceof MixedType && !$acceptingType instanceof TemplateMixedType - ); + if ($acceptingType instanceof self) { + return TrinaryLogic::createYes(); + } + if ($acceptingType instanceof MixedType && !$acceptingType instanceof TemplateMixedType) { + return TrinaryLogic::createYes(); + } + + return TrinaryLogic::createMaybe(); } public function isSuperTypeOf(Type $type): TrinaryLogic { - return TrinaryLogic::createFromBoolean($type instanceof self); + return TrinaryLogic::createYes(); } public function isSubTypeOf(Type $otherType): TrinaryLogic { - return TrinaryLogic::createFromBoolean($otherType instanceof self); + if ($otherType instanceof self) { + return TrinaryLogic::createYes(); + } + if ($otherType instanceof MixedType && !$otherType instanceof TemplateMixedType) { + return TrinaryLogic::createYes(); + } + + return TrinaryLogic::createMaybe(); } public function equals(Type $type): bool @@ -68,12 +85,12 @@ public function hasProperty(string $propertyName): TrinaryLogic public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function canCallMethods(): TrinaryLogic @@ -88,12 +105,12 @@ public function hasMethod(string $methodName): TrinaryLogic public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function canAccessConstants(): TrinaryLogic @@ -108,7 +125,7 @@ public function hasConstant(string $constantName): TrinaryLogic public function getConstant(string $constantName): ConstantReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function isIterable(): TrinaryLogic @@ -136,11 +153,26 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNumericString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isOffsetAccessible(): TrinaryLogic { return TrinaryLogic::createNo(); @@ -161,6 +193,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return new ErrorType(); } + public function unsetOffset(Type $offsetType): Type + { + return new ErrorType(); + } + public function isCallable(): TrinaryLogic { return TrinaryLogic::createNo(); @@ -223,7 +260,6 @@ public function traverse(callable $cb): Type /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { diff --git a/src/Type/StringAlwaysAcceptingObjectWithToStringType.php b/src/Type/StringAlwaysAcceptingObjectWithToStringType.php index 1be5164838..fbfd0b9b99 100644 --- a/src/Type/StringAlwaysAcceptingObjectWithToStringType.php +++ b/src/Type/StringAlwaysAcceptingObjectWithToStringType.php @@ -2,7 +2,7 @@ namespace PHPStan\Type; -use PHPStan\Broker\Broker; +use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\TrinaryLogic; class StringAlwaysAcceptingObjectWithToStringType extends StringType @@ -11,14 +11,14 @@ class StringAlwaysAcceptingObjectWithToStringType extends StringType public function accepts(Type $type, bool $strictTypes): TrinaryLogic { if ($type instanceof TypeWithClassName) { - $broker = Broker::getInstance(); - if (!$broker->hasClass($type->getClassName())) { + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + if (!$reflectionProvider->hasClass($type->getClassName())) { return TrinaryLogic::createNo(); } - $typeClass = $broker->getClass($type->getClassName()); + $typeClass = $reflectionProvider->getClass($type->getClassName()); return TrinaryLogic::createFromBoolean( - $typeClass->hasNativeMethod('__toString') + $typeClass->hasNativeMethod('__toString'), ); } diff --git a/src/Type/StringType.php b/src/Type/StringType.php index 213a6e0aec..f151910b3d 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -2,11 +2,14 @@ namespace PHPStan\Type; -use PHPStan\Broker\Broker; +use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Traits\MaybeCallableTypeTrait; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; @@ -24,6 +27,7 @@ class StringType implements Type use UndecidedBooleanTypeTrait; use UndecidedComparisonTypeTrait; use NonGenericTypeTrait; + use NonGeneralizableTypeTrait; /** @api */ public function __construct() @@ -72,6 +76,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return new ErrorType(); } + public function unsetOffset(Type $offsetType): Type + { + return new ErrorType(); + } + public function accepts(Type $type, bool $strictTypes): TrinaryLogic { if ($type instanceof self) { @@ -79,18 +88,18 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic } if ($type instanceof CompoundType) { - return CompoundTypeHelper::accepts($type, $this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } if ($type instanceof TypeWithClassName && !$strictTypes) { - $broker = Broker::getInstance(); - if (!$broker->hasClass($type->getClassName())) { + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + if (!$reflectionProvider->hasClass($type->getClassName())) { return TrinaryLogic::createNo(); } - $typeClass = $broker->getClass($type->getClassName()); + $typeClass = $reflectionProvider->getClass($type->getClassName()); return TrinaryLogic::createFromBoolean( - $typeClass->hasNativeMethod('__toString') + $typeClass->hasNativeMethod('__toString'), ); } @@ -122,18 +131,44 @@ public function toArray(): Type return new ConstantArrayType( [new ConstantIntegerType(0)], [$this], - 1 + 1, ); } + public function isString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + public function isNumericString(): TrinaryLogic { return TrinaryLogic::createMaybe(); } + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function tryRemove(Type $typeToRemove): ?Type + { + if ($typeToRemove instanceof ConstantStringType && $typeToRemove->getValue() === '') { + return TypeCombinator::intersect($this, new AccessoryNonEmptyStringType()); + } + if ($typeToRemove instanceof AccessoryNonEmptyStringType) { + return new ConstantStringType(''); + } + + return null; + } + /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { diff --git a/src/Type/ThisType.php b/src/Type/ThisType.php index 2d348f2abe..3eb2f6d83c 100644 --- a/src/Type/ThisType.php +++ b/src/Type/ThisType.php @@ -3,6 +3,8 @@ namespace PHPStan\Type; use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ReflectionProviderStaticAccessor; +use function sprintf; /** @api */ class ThisType extends StaticType @@ -10,25 +12,36 @@ class ThisType extends StaticType /** * @api - * @param string|ClassReflection $classReflection */ - public function __construct($classReflection) + public function __construct( + ClassReflection $classReflection, + ?Type $subtractedType = null, + ) { - parent::__construct($classReflection); + parent::__construct($classReflection, $subtractedType); } - /** - * @param ClassReflection|string $classReflection - * @return self - */ - public function changeBaseClass($classReflection): StaticType + public function changeBaseClass(ClassReflection $classReflection): StaticType { - return new self($classReflection); + return new self($classReflection, $this->getSubtractedType()); } public function describe(VerbosityLevel $level): string { - return sprintf('$this(%s)', $this->getClassName()); + return sprintf('$this(%s)', $this->getStaticObjectType()->describe($level)); + } + + /** + * @param mixed[] $properties + */ + public static function __set_state(array $properties): Type + { + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + if ($reflectionProvider->hasClass($properties['baseClass'])) { + return new self($reflectionProvider->getClass($properties['baseClass']), $properties['subtractedType'] ?? null); + } + + return new ErrorType(); } } diff --git a/src/Type/Traits/ConstantScalarTypeTrait.php b/src/Type/Traits/ConstantScalarTypeTrait.php index fc24cc9abc..44e8674c39 100644 --- a/src/Type/Traits/ConstantScalarTypeTrait.php +++ b/src/Type/Traits/ConstantScalarTypeTrait.php @@ -4,8 +4,8 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\CompoundType; -use PHPStan\Type\CompoundTypeHelper; use PHPStan\Type\ConstantScalarType; +use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\Type; trait ConstantScalarTypeTrait @@ -18,7 +18,7 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic } if ($type instanceof CompoundType) { - return CompoundTypeHelper::accepts($type, $this, $strictTypes); + return $type->isAcceptedBy($this, $strictTypes); } return TrinaryLogic::createNo(); @@ -72,7 +72,7 @@ public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic return TrinaryLogic::createMaybe(); } - public function generalize(): Type + public function generalize(GeneralizePrecision $precision): Type { return new parent(); } diff --git a/src/Type/Traits/MaybeCallableTypeTrait.php b/src/Type/Traits/MaybeCallableTypeTrait.php index 69bba1af5f..a6f5ee2ec3 100644 --- a/src/Type/Traits/MaybeCallableTypeTrait.php +++ b/src/Type/Traits/MaybeCallableTypeTrait.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\Traits; use PHPStan\Reflection\ClassMemberAccessAnswerer; +use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\TrinaryLogic; @@ -15,8 +16,7 @@ public function isCallable(): TrinaryLogic } /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] + * @return ParametersAcceptor[] */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { diff --git a/src/Type/Traits/MaybeObjectTypeTrait.php b/src/Type/Traits/MaybeObjectTypeTrait.php index 5ed46f4aeb..607e9360ee 100644 --- a/src/Type/Traits/MaybeObjectTypeTrait.php +++ b/src/Type/Traits/MaybeObjectTypeTrait.php @@ -41,9 +41,7 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember $property, $property->getDeclaringClass(), false, - static function (Type $type): Type { - return $type; - } + static fn (Type $type): Type => $type, ); } @@ -69,9 +67,7 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce $method, $method->getDeclaringClass(), false, - static function (Type $type): Type { - return $type; - } + static fn (Type $type): Type => $type, ); } diff --git a/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php b/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php index 755bf8c44e..55889eb574 100644 --- a/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php +++ b/src/Type/Traits/MaybeOffsetAccessibleTypeTrait.php @@ -29,4 +29,9 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } + public function unsetOffset(Type $offsetType): Type + { + return $this; + } + } diff --git a/src/Type/Traits/NonCallableTypeTrait.php b/src/Type/Traits/NonCallableTypeTrait.php index 75a6233d4c..e64719b1d1 100644 --- a/src/Type/Traits/NonCallableTypeTrait.php +++ b/src/Type/Traits/NonCallableTypeTrait.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\Traits; use PHPStan\Reflection\ClassMemberAccessAnswerer; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; trait NonCallableTypeTrait @@ -15,7 +16,7 @@ public function isCallable(): TrinaryLogic public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } } diff --git a/src/Type/Traits/NonGeneralizableTypeTrait.php b/src/Type/Traits/NonGeneralizableTypeTrait.php new file mode 100644 index 0000000000..e943051c95 --- /dev/null +++ b/src/Type/Traits/NonGeneralizableTypeTrait.php @@ -0,0 +1,16 @@ +traverse(static fn (Type $type) => $type->generalize($precision)); + } + +} diff --git a/src/Type/Traits/NonObjectTypeTrait.php b/src/Type/Traits/NonObjectTypeTrait.php index b4ea902759..a768d24f41 100644 --- a/src/Type/Traits/NonObjectTypeTrait.php +++ b/src/Type/Traits/NonObjectTypeTrait.php @@ -8,6 +8,7 @@ use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; +use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; trait NonObjectTypeTrait @@ -25,12 +26,12 @@ public function hasProperty(string $propertyName): TrinaryLogic public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function canCallMethods(): TrinaryLogic @@ -45,12 +46,12 @@ public function hasMethod(string $methodName): TrinaryLogic public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function canAccessConstants(): TrinaryLogic @@ -65,7 +66,7 @@ public function hasConstant(string $constantName): TrinaryLogic public function getConstant(string $constantName): ConstantReflection { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function isCloneable(): TrinaryLogic diff --git a/src/Type/Traits/NonOffsetAccessibleTypeTrait.php b/src/Type/Traits/NonOffsetAccessibleTypeTrait.php index 68a703d069..929d8c0002 100644 --- a/src/Type/Traits/NonOffsetAccessibleTypeTrait.php +++ b/src/Type/Traits/NonOffsetAccessibleTypeTrait.php @@ -26,7 +26,12 @@ public function getOffsetValueType(Type $offsetType): Type public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { - return $this; + return new ErrorType(); + } + + public function unsetOffset(Type $offsetType): Type + { + return new ErrorType(); } } diff --git a/src/Type/Traits/NonRemoveableTypeTrait.php b/src/Type/Traits/NonRemoveableTypeTrait.php new file mode 100644 index 0000000000..1eb40ea378 --- /dev/null +++ b/src/Type/Traits/NonRemoveableTypeTrait.php @@ -0,0 +1,15 @@ +getDeclaringClass(), false, - static function (Type $type): Type { - return $type; - } + static fn (Type $type): Type => $type, ); } @@ -78,9 +76,7 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce $method, $method->getDeclaringClass(), false, - static function (Type $type): Type { - return $type; - } + static fn (Type $type): Type => $type, ); } @@ -109,11 +105,26 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNumericString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function toNumber(): Type { return new ErrorType(); diff --git a/src/Type/Type.php b/src/Type/Type.php index acb40b2093..44d71ef154 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -5,6 +5,7 @@ use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; @@ -70,11 +71,12 @@ public function getOffsetValueType(Type $offsetType): Type; public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type; + public function unsetOffset(Type $offsetType): Type; + public function isCallable(): TrinaryLogic; /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] + * @return ParametersAcceptor[] */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array; @@ -96,8 +98,14 @@ public function isSmallerThan(Type $otherType): TrinaryLogic; public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic; + public function isString(): TrinaryLogic; + public function isNumericString(): TrinaryLogic; + public function isNonEmptyString(): TrinaryLogic; + + public function isLiteralString(): TrinaryLogic; + public function getSmallerType(): Type; public function getSmallerOrEqualType(): Type; @@ -142,9 +150,17 @@ public function getReferencedTemplateTypes(TemplateTypeVariance $positionVarianc */ public function traverse(callable $cb): Type; + /** + * Return the difference with another type, or null if it cannot be represented. + * + * @see TypeCombinator::remove() + */ + public function tryRemove(Type $typeToRemove): ?Type; + + public function generalize(GeneralizePrecision $precision): Type; + /** * @param mixed[] $properties - * @return self */ public static function __set_state(array $properties): self; diff --git a/src/Type/TypeAlias.php b/src/Type/TypeAlias.php index 48a59ec40e..10d6c09922 100644 --- a/src/Type/TypeAlias.php +++ b/src/Type/TypeAlias.php @@ -10,25 +10,19 @@ class TypeAlias { - private TypeNode $typeNode; - - private NameScope $nameScope; - private ?Type $resolvedType = null; public function __construct( - TypeNode $typeNode, - NameScope $nameScope + private TypeNode $typeNode, + private NameScope $nameScope, ) { - $this->typeNode = $typeNode; - $this->nameScope = $nameScope; } public static function invalid(): self { $self = new self(new IdentifierTypeNode('*ERROR*'), new NameScope(null, [])); - $self->resolvedType = new ErrorType(); + $self->resolvedType = new CircularTypeAliasErrorType(); return $self; } @@ -37,7 +31,7 @@ public function resolve(TypeNodeResolver $typeNodeResolver): Type if ($this->resolvedType === null) { $this->resolvedType = $typeNodeResolver->resolve( $this->typeNode, - $this->nameScope + $this->nameScope, ); } diff --git a/src/Type/TypeAliasResolver.php b/src/Type/TypeAliasResolver.php index 1ce2e8f215..af6be470cc 100644 --- a/src/Type/TypeAliasResolver.php +++ b/src/Type/TypeAliasResolver.php @@ -3,162 +3,12 @@ namespace PHPStan\Type; use PHPStan\Analyser\NameScope; -use PHPStan\PhpDoc\TypeNodeResolver; -use PHPStan\PhpDoc\TypeStringResolver; -use PHPStan\Reflection\ReflectionProvider; -use function array_key_exists; -class TypeAliasResolver +interface TypeAliasResolver { - /** @var array */ - private array $globalTypeAliases; + public function hasTypeAlias(string $aliasName, ?string $classNameScope): bool; - private TypeStringResolver $typeStringResolver; - - private TypeNodeResolver $typeNodeResolver; - - private ReflectionProvider $reflectionProvider; - - /** @var array */ - private array $resolvedGlobalTypeAliases = []; - - /** @var array */ - private array $resolvedLocalTypeAliases = []; - - /** @var array */ - private array $resolvingClassTypeAliases = []; - - /** @var array */ - private array $inProcess = []; - - /** - * @param array $globalTypeAliases - */ - public function __construct( - array $globalTypeAliases, - TypeStringResolver $typeStringResolver, - TypeNodeResolver $typeNodeResolver, - ReflectionProvider $reflectionProvider - ) - { - $this->globalTypeAliases = $globalTypeAliases; - $this->typeStringResolver = $typeStringResolver; - $this->typeNodeResolver = $typeNodeResolver; - $this->reflectionProvider = $reflectionProvider; - } - - public function hasTypeAlias(string $aliasName, ?string $classNameScope): bool - { - $hasGlobalTypeAlias = array_key_exists($aliasName, $this->globalTypeAliases); - if ($hasGlobalTypeAlias) { - return true; - } - - if ($classNameScope === null || !$this->reflectionProvider->hasClass($classNameScope)) { - return false; - } - - $classReflection = $this->reflectionProvider->getClass($classNameScope); - $localTypeAliases = $classReflection->getTypeAliases(); - return array_key_exists($aliasName, $localTypeAliases); - } - - public function resolveTypeAlias(string $aliasName, NameScope $nameScope): ?Type - { - return $this->resolveLocalTypeAlias($aliasName, $nameScope) - ?? $this->resolveGlobalTypeAlias($aliasName, $nameScope); - } - - private function resolveLocalTypeAlias(string $aliasName, NameScope $nameScope): ?Type - { - if (array_key_exists($aliasName, $this->globalTypeAliases)) { - return null; - } - - if (!$nameScope->hasTypeAlias($aliasName)) { - return null; - } - - $className = $nameScope->getClassName(); - if ($className === null) { - return null; - } - - $aliasNameInClassScope = $className . '::' . $aliasName; - - if (array_key_exists($aliasNameInClassScope, $this->resolvedLocalTypeAliases)) { - return $this->resolvedLocalTypeAliases[$aliasNameInClassScope]; - } - - // prevent infinite recursion - if (array_key_exists($className, $this->resolvingClassTypeAliases)) { - return null; - } - - $this->resolvingClassTypeAliases[$className] = true; - - if (!$this->reflectionProvider->hasClass($className)) { - unset($this->resolvingClassTypeAliases[$className]); - return null; - } - - $classReflection = $this->reflectionProvider->getClass($className); - $localTypeAliases = $classReflection->getTypeAliases(); - - unset($this->resolvingClassTypeAliases[$className]); - - if (!array_key_exists($aliasName, $localTypeAliases)) { - return null; - } - - if (array_key_exists($aliasNameInClassScope, $this->inProcess)) { - // resolve circular reference as ErrorType to make it easier to detect - throw new \PHPStan\Type\CircularTypeAliasDefinitionException(); - } - - $this->inProcess[$aliasNameInClassScope] = true; - - try { - $unresolvedAlias = $localTypeAliases[$aliasName]; - $resolvedAliasType = $unresolvedAlias->resolve($this->typeNodeResolver); - } catch (\PHPStan\Type\CircularTypeAliasDefinitionException $e) { - $resolvedAliasType = new ErrorType(); - } - - $this->resolvedLocalTypeAliases[$aliasNameInClassScope] = $resolvedAliasType; - unset($this->inProcess[$aliasNameInClassScope]); - - return $resolvedAliasType; - } - - private function resolveGlobalTypeAlias(string $aliasName, NameScope $nameScope): ?Type - { - if (!array_key_exists($aliasName, $this->globalTypeAliases)) { - return null; - } - - if (array_key_exists($aliasName, $this->resolvedGlobalTypeAliases)) { - return $this->resolvedGlobalTypeAliases[$aliasName]; - } - - if ($this->reflectionProvider->hasClass($nameScope->resolveStringName($aliasName))) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Type alias %s already exists as a class.', $aliasName)); - } - - if (array_key_exists($aliasName, $this->inProcess)) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Circular definition for type alias %s.', $aliasName)); - } - - $this->inProcess[$aliasName] = true; - - $aliasTypeString = $this->globalTypeAliases[$aliasName]; - $aliasType = $this->typeStringResolver->resolve($aliasTypeString); - $this->resolvedGlobalTypeAliases[$aliasName] = $aliasType; - - unset($this->inProcess[$aliasName]); - - return $aliasType; - } + public function resolveTypeAlias(string $aliasName, NameScope $nameScope): ?Type; } diff --git a/src/Type/TypeAliasResolverProvider.php b/src/Type/TypeAliasResolverProvider.php new file mode 100644 index 0000000000..a0a57ef1ac --- /dev/null +++ b/src/Type/TypeAliasResolverProvider.php @@ -0,0 +1,10 @@ +isSuperTypeOf($type)->no()) { + return self::union($type, new NullType()); + } + + return $type; } public static function remove(Type $fromType, Type $typeToRemove): Type @@ -38,15 +50,6 @@ public static function remove(Type $fromType, Type $typeToRemove): Type return $fromType; } - if ($fromType instanceof UnionType) { - $innerTypes = []; - foreach ($fromType->getTypes() as $innerType) { - $innerTypes[] = self::remove($innerType, $typeToRemove); - } - - return self::union(...$innerTypes); - } - $isSuperType = $typeToRemove->isSuperTypeOf($fromType); if ($isSuperType->yes()) { return new NeverType(); @@ -62,68 +65,7 @@ public static function remove(Type $fromType, Type $typeToRemove): Type } } - if ($fromType instanceof BooleanType) { - if ($typeToRemove instanceof ConstantBooleanType) { - return new ConstantBooleanType(!$typeToRemove->getValue()); - } - } elseif ($fromType instanceof IterableType) { - $arrayType = new ArrayType(new MixedType(), new MixedType()); - if ($typeToRemove->isSuperTypeOf($arrayType)->yes()) { - return new GenericObjectType(\Traversable::class, [ - $fromType->getIterableKeyType(), - $fromType->getIterableValueType(), - ]); - } - - $traversableType = new ObjectType(\Traversable::class); - if ($typeToRemove->isSuperTypeOf($traversableType)->yes()) { - return new ArrayType($fromType->getIterableKeyType(), $fromType->getIterableValueType()); - } - } elseif ($fromType instanceof IntegerRangeType) { - $type = $fromType->tryRemove($typeToRemove); - if ($type !== null) { - return $type; - } - } elseif ($fromType instanceof IntegerType) { - if ($typeToRemove instanceof IntegerRangeType || $typeToRemove instanceof ConstantIntegerType) { - if ($typeToRemove instanceof IntegerRangeType) { - $removeValueMin = $typeToRemove->getMin(); - $removeValueMax = $typeToRemove->getMax(); - } else { - $removeValueMin = $typeToRemove->getValue(); - $removeValueMax = $typeToRemove->getValue(); - } - $lowerPart = $removeValueMin !== null ? IntegerRangeType::fromInterval(null, $removeValueMin, -1) : null; - $upperPart = $removeValueMax !== null ? IntegerRangeType::fromInterval($removeValueMax, null, +1) : null; - if ($lowerPart !== null && $upperPart !== null) { - return self::union($lowerPart, $upperPart); - } - return $lowerPart ?? $upperPart ?? new NeverType(); - } - } elseif ($fromType->isArray()->yes()) { - if ($typeToRemove instanceof ConstantArrayType && $typeToRemove->isIterableAtLeastOnce()->no()) { - return self::intersect($fromType, new NonEmptyArrayType()); - } - - if ($typeToRemove instanceof NonEmptyArrayType) { - return new ConstantArrayType([], []); - } - - if ($fromType instanceof ConstantArrayType && $typeToRemove instanceof HasOffsetType) { - return $fromType->unsetOffset($typeToRemove->getOffsetType()); - } - } elseif ($fromType instanceof SubtractableType) { - $typeToSubtractFrom = $fromType; - if ($fromType instanceof TemplateType) { - $typeToSubtractFrom = $fromType->getBound(); - } - - if ($typeToSubtractFrom->isSuperTypeOf($typeToRemove)->yes()) { - return $fromType->subtract($typeToRemove); - } - } - - return $fromType; + return $fromType->tryRemove($typeToRemove) ?? $fromType; } public static function removeNull(Type $type): Type @@ -152,28 +94,38 @@ public static function containsNull(Type $type): bool public static function union(Type ...$types): Type { + $typesCount = count($types); + $benevolentTypes = []; $benevolentUnionObject = null; // transform A | (B | C) to A | B | C - for ($i = 0; $i < count($types); $i++) { + for ($i = 0; $i < $typesCount; $i++) { if ($types[$i] instanceof BenevolentUnionType) { if ($types[$i] instanceof TemplateBenevolentUnionType && $benevolentUnionObject === null) { $benevolentUnionObject = $types[$i]; } - foreach ($types[$i]->getTypes() as $benevolentInnerType) { + $benevolentTypesCount = 0; + $typesInner = $types[$i]->getTypes(); + foreach ($typesInner as $benevolentInnerType) { + $benevolentTypesCount++; $benevolentTypes[$benevolentInnerType->describe(VerbosityLevel::value())] = $benevolentInnerType; } - array_splice($types, $i, 1, $types[$i]->getTypes()); + array_splice($types, $i, 1, $typesInner); + $typesCount += $benevolentTypesCount - 1; continue; } if (!($types[$i] instanceof UnionType)) { continue; } + if ($types[$i] instanceof TemplateType) { + continue; + } - array_splice($types, $i, 1, $types[$i]->getTypes()); + $typesInner = $types[$i]->getTypes(); + array_splice($types, $i, 1, $typesInner); + $typesCount += count($typesInner) - 1; } - $typesCount = count($types); $arrayTypes = []; $arrayAccessoryTypes = []; $scalarTypes = []; @@ -231,6 +183,10 @@ public static function union(Type ...$types): Type unset($types[$i]); } + foreach ($scalarTypes as $classType => $scalarTypeItems) { + $scalarTypes[$classType] = array_values($scalarTypeItems); + } + /** @var ArrayType[] $arrayTypes */ $arrayTypes = $arrayTypes; @@ -244,19 +200,21 @@ public static function union(Type ...$types): Type $types = array_values( array_merge( $types, - self::processArrayTypes($arrayTypes, $arrayAccessoryTypesToProcess) - ) + self::processArrayTypes($arrayTypes, $arrayAccessoryTypesToProcess), + ), ); + $typesCount = count($types); // simplify string[] | int[] to (string|int)[] - for ($i = 0; $i < count($types); $i++) { - for ($j = $i + 1; $j < count($types); $j++) { + for ($i = 0; $i < $typesCount; $i++) { + for ($j = $i + 1; $j < $typesCount; $j++) { if ($types[$i] instanceof IterableType && $types[$j] instanceof IterableType) { $types[$i] = new IterableType( self::union($types[$i]->getIterableKeyType(), $types[$j]->getIterableKeyType()), - self::union($types[$i]->getIterableValueType(), $types[$j]->getIterableValueType()) + self::union($types[$i]->getIterableValueType(), $types[$j]->getIterableValueType()), ); array_splice($types, $j, 1); + $typesCount--; continue 2; } } @@ -264,102 +222,92 @@ public static function union(Type ...$types): Type foreach ($scalarTypes as $classType => $scalarTypeItems) { if (isset($hasGenericScalarTypes[$classType])) { + unset($scalarTypes[$classType]); continue; } if ($classType === ConstantBooleanType::class && count($scalarTypeItems) === 2) { $types[] = new BooleanType(); + $typesCount++; + unset($scalarTypes[$classType]); continue; } - foreach ($scalarTypeItems as $type) { - if (count($scalarTypeItems) > self::CONSTANT_SCALAR_UNION_THRESHOLD) { - $types[] = $type->generalize(); - if ($type instanceof ConstantStringType) { + $scalarTypeItemsCount = count($scalarTypeItems); + for ($i = 0; $i < $typesCount; $i++) { + for ($j = 0; $j < $scalarTypeItemsCount; $j++) { + $compareResult = self::compareTypesInUnion($types[$i], $scalarTypeItems[$j]); + if ($compareResult === null) { continue; } - break; + [$a, $b] = $compareResult; + if ($a !== null) { + $types[$i] = $a; + array_splice($scalarTypeItems, $j--, 1); + $scalarTypeItemsCount--; + continue 1; + } + if ($b !== null) { + $scalarTypeItems[$j] = $b; + array_splice($types, $i--, 1); + $typesCount--; + continue 2; + } } - $types[] = $type; } + + $scalarTypes[$classType] = $scalarTypeItems; + } + + if (count($types) > 16) { + $newTypes = []; + foreach ($types as $type) { + $newTypes[$type->describe(VerbosityLevel::cache())] = $type; + } + $types = array_values($newTypes); + $typesCount = count($types); } // transform A | A to A // transform A | never to A - for ($i = 0; $i < count($types); $i++) { - for ($j = $i + 1; $j < count($types); $j++) { - if ($types[$i] instanceof IntegerRangeType) { - $type = $types[$i]->tryUnion($types[$j]); - if ($type !== null) { - $types[$i] = $type; - $i--; - array_splice($types, $j, 1); - continue 2; - } - } - - if ($types[$i] instanceof SubtractableType) { - $typeWithoutSubtractedTypeA = $types[$i]->getTypeWithoutSubtractedType(); - if ($typeWithoutSubtractedTypeA instanceof MixedType && $types[$j] instanceof MixedType) { - $isSuperType = $typeWithoutSubtractedTypeA->isSuperTypeOfMixed($types[$j]); - } else { - $isSuperType = $typeWithoutSubtractedTypeA->isSuperTypeOf($types[$j]); - } - if ($isSuperType->yes()) { - $subtractedType = null; - if ($types[$j] instanceof SubtractableType) { - $subtractedType = $types[$j]->getSubtractedType(); - } - $types[$i] = self::intersectWithSubtractedType($types[$i], $subtractedType); - array_splice($types, $j--, 1); - continue 1; - } + for ($i = 0; $i < $typesCount; $i++) { + for ($j = $i + 1; $j < $typesCount; $j++) { + $compareResult = self::compareTypesInUnion($types[$i], $types[$j]); + if ($compareResult === null) { + continue; } - if ($types[$j] instanceof SubtractableType) { - $typeWithoutSubtractedTypeB = $types[$j]->getTypeWithoutSubtractedType(); - if ($typeWithoutSubtractedTypeB instanceof MixedType && $types[$i] instanceof MixedType) { - $isSuperType = $typeWithoutSubtractedTypeB->isSuperTypeOfMixed($types[$i]); - } else { - $isSuperType = $typeWithoutSubtractedTypeB->isSuperTypeOf($types[$i]); - } - if ($isSuperType->yes()) { - $subtractedType = null; - if ($types[$i] instanceof SubtractableType) { - $subtractedType = $types[$i]->getSubtractedType(); - } - $types[$j] = self::intersectWithSubtractedType($types[$j], $subtractedType); - array_splice($types, $i--, 1); - continue 2; - } + [$a, $b] = $compareResult; + if ($a !== null) { + $types[$i] = $a; + array_splice($types, $j--, 1); + $typesCount--; + continue 1; } - - if ( - !$types[$j] instanceof ConstantArrayType - && $types[$j]->isSuperTypeOf($types[$i])->yes() - ) { + if ($b !== null) { + $types[$j] = $b; array_splice($types, $i--, 1); + $typesCount--; continue 2; } + } + } - if ( - !$types[$i] instanceof ConstantArrayType - && $types[$i]->isSuperTypeOf($types[$j])->yes() - ) { - array_splice($types, $j--, 1); - continue 1; - } + foreach ($scalarTypes as $scalarTypeItems) { + foreach ($scalarTypeItems as $scalarType) { + $types[] = $scalarType; + $typesCount++; } } - if (count($types) === 0) { + if ($typesCount === 0) { return new NeverType(); - - } elseif (count($types) === 1) { + } + if ($typesCount === 1) { return $types[0]; } - if (count($benevolentTypes) > 0) { + if ($benevolentTypes !== []) { $tempTypes = $types; foreach ($tempTypes as $i => $type) { if (!isset($benevolentTypes[$type->describe(VerbosityLevel::value())])) { @@ -369,7 +317,7 @@ public static function union(Type ...$types): Type unset($tempTypes[$i]); } - if (count($tempTypes) === 0) { + if ($tempTypes === []) { if ($benevolentUnionObject instanceof TemplateBenevolentUnionType) { return $benevolentUnionObject->withTypes($types); } @@ -381,9 +329,91 @@ public static function union(Type ...$types): Type return new UnionType($types); } + /** + * @return array{Type, null}|array{null, Type}|null + */ + private static function compareTypesInUnion(Type $a, Type $b): ?array + { + if ($a instanceof IntegerRangeType) { + $type = $a->tryUnion($b); + if ($type !== null) { + $a = $type; + return [$a, null]; + } + } + if ($b instanceof IntegerRangeType) { + $type = $b->tryUnion($a); + if ($type !== null) { + $b = $type; + return [null, $b]; + } + } + if ($a instanceof IntegerRangeType && $b instanceof IntegerRangeType) { + return null; + } + + if ($a instanceof SubtractableType) { + $typeWithoutSubtractedTypeA = $a->getTypeWithoutSubtractedType(); + if ($typeWithoutSubtractedTypeA instanceof MixedType && $b instanceof MixedType) { + $isSuperType = $typeWithoutSubtractedTypeA->isSuperTypeOfMixed($b); + } else { + $isSuperType = $typeWithoutSubtractedTypeA->isSuperTypeOf($b); + } + if ($isSuperType->yes()) { + $a = self::intersectWithSubtractedType($a, $b); + return [$a, null]; + } + } + + if ($b instanceof SubtractableType) { + $typeWithoutSubtractedTypeB = $b->getTypeWithoutSubtractedType(); + if ($typeWithoutSubtractedTypeB instanceof MixedType && $a instanceof MixedType) { + $isSuperType = $typeWithoutSubtractedTypeB->isSuperTypeOfMixed($a); + } else { + $isSuperType = $typeWithoutSubtractedTypeB->isSuperTypeOf($a); + } + if ($isSuperType->yes()) { + $b = self::intersectWithSubtractedType($b, $a); + return [null, $b]; + } + } + + if ( + !$b instanceof ConstantArrayType + && $b->isSuperTypeOf($a)->yes() + ) { + return [null, $b]; + } + + if ( + !$a instanceof ConstantArrayType + && $a->isSuperTypeOf($b)->yes() + ) { + return [$a, null]; + } + + if ( + $a instanceof ConstantStringType + && $a->getValue() === '' + && $b->describe(VerbosityLevel::value()) === 'non-empty-string' + ) { + return [null, new StringType()]; + } + + if ( + $b instanceof ConstantStringType + && $b->getValue() === '' + && $a->describe(VerbosityLevel::value()) === 'non-empty-string' + ) { + return [new StringType(), null]; + } + + return null; + } + private static function unionWithSubtractedType( Type $type, - ?Type $subtractedType + ?Type $subtractedType, ): Type { if ($subtractedType === null) { @@ -397,7 +427,7 @@ private static function unionWithSubtractedType( $subtractedType = self::union( $type->getSubtractedType(), - $subtractedType + $subtractedType, ); if ($subtractedType instanceof NeverType) { $subtractedType = null; @@ -414,27 +444,36 @@ private static function unionWithSubtractedType( } private static function intersectWithSubtractedType( - SubtractableType $subtractableType, - ?Type $subtractedType + SubtractableType $a, + Type $b, ): Type { - if ($subtractableType->getSubtractedType() === null) { - return $subtractableType; + if ($a->getSubtractedType() === null) { + return $a; } - if ($subtractedType === null) { - return $subtractableType->getTypeWithoutSubtractedType(); + if ($b instanceof SubtractableType) { + $subtractedType = $b->getSubtractedType(); + if ($subtractedType === null) { + return $a->getTypeWithoutSubtractedType(); + } + } else { + $subtractedTypeTmp = self::intersect($a->getTypeWithoutSubtractedType(), $a->getSubtractedType()); + if ($b->isSuperTypeOf($subtractedTypeTmp)->yes()) { + return $a->getTypeWithoutSubtractedType(); + } + $subtractedType = new MixedType(false, $b); } $subtractedType = self::intersect( - $subtractableType->getSubtractedType(), - $subtractedType + $a->getSubtractedType(), + $subtractedType, ); if ($subtractedType instanceof NeverType) { $subtractedType = null; } - return $subtractableType->changeSubtractedType($subtractedType); + return $a->changeSubtractedType($subtractedType); } /** @@ -461,9 +500,11 @@ private static function processArrayTypes(array $arrayTypes, array $accessoryTyp break 2; } } - if (count($arrayTypes) === 0) { + + if ($arrayTypes === []) { return []; - } elseif (count($arrayTypes) === 1) { + } + if (count($arrayTypes) === 1) { return [ self::intersect($arrayTypes[0], ...$accessoryTypes), ]; @@ -507,7 +548,7 @@ private static function processArrayTypes(array $arrayTypes, array $accessoryTyp return [ self::intersect(new ArrayType( self::union(...$keyTypesForGeneralArray), - self::union(...$valueTypesForGeneralArray) + self::union(...$valueTypesForGeneralArray), ), ...$accessoryTypes), ]; } @@ -542,7 +583,7 @@ private static function processArrayTypes(array $arrayTypes, array $accessoryTyp foreach ($arrayTypeAgain->getKeyTypes() as $i => $keyType) { $bucket[$keyType->getValue()]['valueType'] = self::union( $bucket[$keyType->getValue()]['valueType'], - $arrayTypeAgain->getValueTypes()[$i] + $arrayTypeAgain->getValueTypes()[$i], ); $bucket[$keyType->getValue()]['optional'] = $bucket[$keyType->getValue()]['optional'] || $arrayTypeAgain->isOptionalKey($i); } @@ -580,16 +621,18 @@ private static function reduceArrays(array $constantArrays): array $arraysToProcess[] = $constantArray; } - for ($i = 0; $i < count($arraysToProcess); $i++) { - for ($j = $i + 1; $j < count($arraysToProcess); $j++) { + for ($i = 0, $arraysToProcessCount = count($arraysToProcess); $i < $arraysToProcessCount; $i++) { + for ($j = $i + 1; $j < $arraysToProcessCount; $j++) { if ($arraysToProcess[$j]->isKeysSupersetOf($arraysToProcess[$i])) { $arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]); array_splice($arraysToProcess, $i--, 1); + $arraysToProcessCount--; continue 2; } elseif ($arraysToProcess[$i]->isKeysSupersetOf($arraysToProcess[$j])) { $arraysToProcess[$i] = $arraysToProcess[$i]->mergeWith($arraysToProcess[$j]); array_splice($arraysToProcess, $j--, 1); + $arraysToProcessCount--; continue 1; } } @@ -600,6 +643,8 @@ private static function reduceArrays(array $constantArrays): array public static function intersect(Type ...$types): Type { + $types = array_values($types); + $sortTypes = static function (Type $a, Type $b): int { if (!$a instanceof UnionType || !$b instanceof UnionType) { return 0; @@ -635,7 +680,7 @@ public static function intersect(Type ...$types): Type $topLevelUnionSubTypes[] = self::intersect( $innerUnionSubType, ...array_slice($types, 0, $i), - ...array_slice($types, $i + 1) + ...array_slice($types, $i + 1), ); } @@ -649,7 +694,7 @@ public static function intersect(Type ...$types): Type $type->getScope(), $type->getName(), $union, - $type->getVariance() + $type->getVariance(), ); if ($type->isArgument()) { return TemplateTypeHelper::toArgument($union); @@ -658,17 +703,31 @@ public static function intersect(Type ...$types): Type return $union; } + $typesCount = count($types); // transform A & (B & C) to A & B & C - for ($i = 0; $i < count($types); $i++) { + for ($i = 0; $i < $typesCount; $i++) { $type = $types[$i]; if (!($type instanceof IntersectionType)) { continue; } array_splice($types, $i--, 1, $type->getTypes()); + $typesCount = count($types); } + // move subtractables with subtracts before those without to avoid loosing them in the union logic + usort($types, static function (Type $a, Type $b): int { + if ($a instanceof SubtractableType && $a->getSubtractedType() !== null) { + return -1; + } + if ($b instanceof SubtractableType && $b->getSubtractedType() !== null) { + return 1; + } + + return 0; + }); + // transform IntegerType & ConstantIntegerType to ConstantIntegerType // transform Child & Parent to Child // transform Object & ~null to Object @@ -677,8 +736,8 @@ public static function intersect(Type ...$types): Type // transform callable & int to never // transform A & ~A to never // transform int & string to never - for ($i = 0; $i < count($types); $i++) { - for ($j = $i + 1; $j < count($types); $j++) { + for ($i = 0; $i < $typesCount; $i++) { + for ($j = $i + 1; $j < $typesCount; $j++) { if ($types[$j] instanceof SubtractableType) { $typeWithoutSubtractedTypeA = $types[$j]->getTypeWithoutSubtractedType(); @@ -690,6 +749,7 @@ public static function intersect(Type ...$types): Type if ($isSuperTypeSubtractableA->yes()) { $types[$i] = self::unionWithSubtractedType($types[$i], $types[$j]->getSubtractedType()); array_splice($types, $j--, 1); + $typesCount--; continue 1; } } @@ -705,6 +765,7 @@ public static function intersect(Type ...$types): Type if ($isSuperTypeSubtractableB->yes()) { $types[$j] = self::unionWithSubtractedType($types[$j], $types[$i]->getSubtractedType()); array_splice($types, $i--, 1); + $typesCount--; continue 2; } } @@ -714,6 +775,7 @@ public static function intersect(Type ...$types): Type if ($intersectionType !== null) { $types[$j] = $intersectionType; array_splice($types, $i--, 1); + $typesCount--; continue 2; } } @@ -726,6 +788,7 @@ public static function intersect(Type ...$types): Type if ($isSuperTypeA->yes()) { array_splice($types, $j--, 1); + $typesCount--; continue; } @@ -739,12 +802,14 @@ public static function intersect(Type ...$types): Type if ($types[$i] instanceof ConstantArrayType && $types[$j] instanceof HasOffsetType) { $types[$i] = $types[$i]->makeOffsetRequired($types[$j]->getOffsetType()); array_splice($types, $j--, 1); + $typesCount--; continue; } if ($types[$j] instanceof ConstantArrayType && $types[$i] instanceof HasOffsetType) { $types[$j] = $types[$j]->makeOffsetRequired($types[$i]->getOffsetType()); array_splice($types, $i--, 1); + $typesCount--; continue 2; } @@ -760,6 +825,7 @@ public static function intersect(Type ...$types): Type $types[$j] = new ArrayType($keyType, $itemType); } array_splice($types, $i--, 1); + $typesCount--; continue 2; } @@ -768,6 +834,7 @@ public static function intersect(Type ...$types): Type if ($isSuperTypeB->yes()) { array_splice($types, $i--, 1); + $typesCount--; continue 2; } @@ -777,9 +844,8 @@ public static function intersect(Type ...$types): Type } } - if (count($types) === 1) { + if ($typesCount === 1) { return $types[0]; - } return new IntersectionType($types); diff --git a/src/Type/TypeUtils.php b/src/Type/TypeUtils.php index 70fcb4cc86..91dcc50bb0 100644 --- a/src/Type/TypeUtils.php +++ b/src/Type/TypeUtils.php @@ -6,14 +6,14 @@ use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantStringType; +use function array_merge; /** @api */ class TypeUtils { /** - * @param \PHPStan\Type\Type $type - * @return \PHPStan\Type\ArrayType[] + * @return ArrayType[] */ public static function getArrays(Type $type): array { @@ -57,8 +57,7 @@ public static function getArrays(Type $type): array } /** - * @param \PHPStan\Type\Type $type - * @return \PHPStan\Type\Constant\ConstantArrayType[] + * @return ConstantArrayType[] */ public static function getConstantArrays(Type $type): array { @@ -84,8 +83,7 @@ public static function getConstantArrays(Type $type): array } /** - * @param \PHPStan\Type\Type $type - * @return \PHPStan\Type\Constant\ConstantStringType[] + * @return ConstantStringType[] */ public static function getConstantStrings(Type $type): array { @@ -93,8 +91,7 @@ public static function getConstantStrings(Type $type): array } /** - * @param \PHPStan\Type\Type $type - * @return \PHPStan\Type\ConstantType[] + * @return ConstantType[] */ public static function getConstantTypes(Type $type): array { @@ -102,8 +99,7 @@ public static function getConstantTypes(Type $type): array } /** - * @param \PHPStan\Type\Type $type - * @return \PHPStan\Type\ConstantType[] + * @return ConstantType[] */ public static function getAnyConstantTypes(Type $type): array { @@ -111,27 +107,22 @@ public static function getAnyConstantTypes(Type $type): array } /** - * @param \PHPStan\Type\Type $type - * @return \PHPStan\Type\ArrayType[] + * @return ArrayType[] */ public static function getAnyArrays(Type $type): array { return self::map(ArrayType::class, $type, true, false); } - public static function generalizeType(Type $type): Type + /** + * @deprecated Use PHPStan\Type\Type::generalize() instead. + */ + public static function generalizeType(Type $type, GeneralizePrecision $precision): Type { - return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { - if ($type instanceof ConstantType) { - return $type->generalize(); - } - - return $traverse($type); - }); + return $type->generalize($precision); } /** - * @param Type $type * @return string[] */ public static function getDirectClassNames(Type $type): array @@ -157,8 +148,15 @@ public static function getDirectClassNames(Type $type): array } /** - * @param Type $type - * @return \PHPStan\Type\ConstantScalarType[] + * @return IntegerRangeType[] + */ + public static function getIntegerRanges(Type $type): array + { + return self::map(IntegerRangeType::class, $type, false); + } + + /** + * @return ConstantScalarType[] */ public static function getConstantScalars(Type $type): array { @@ -167,7 +165,6 @@ public static function getConstantScalars(Type $type): array /** * @internal - * @param Type $type * @return ConstantArrayType[] */ public static function getOldConstantArrays(Type $type): array @@ -176,17 +173,13 @@ public static function getOldConstantArrays(Type $type): array } /** - * @param string $typeClass - * @param Type $type - * @param bool $inspectIntersections - * @param bool $stopOnUnmatched * @return mixed[] */ private static function map( string $typeClass, Type $type, bool $inspectIntersections, - bool $stopOnUnmatched = true + bool $stopOnUnmatched = true, ): array { if ($type instanceof $typeClass) { @@ -244,7 +237,6 @@ public static function toBenevolentUnion(Type $type): Type } /** - * @param Type $type * @return Type[] */ public static function flattenTypes(Type $type): array @@ -291,7 +283,6 @@ public static function findThisType(Type $type): ?ThisType } /** - * @param Type $type * @return HasPropertyType[] */ public static function getHasPropertyTypes(Type $type): array @@ -313,8 +304,7 @@ public static function getHasPropertyTypes(Type $type): array } /** - * @param \PHPStan\Type\Type $type - * @return \PHPStan\Type\Accessory\AccessoryType[] + * @return AccessoryType[] */ public static function getAccessoryTypes(Type $type): array { diff --git a/src/Type/TypehintHelper.php b/src/Type/TypehintHelper.php index abffa0ae92..db139aab8b 100644 --- a/src/Type/TypehintHelper.php +++ b/src/Type/TypehintHelper.php @@ -2,12 +2,20 @@ namespace PHPStan\Type; -use PHPStan\Broker\Broker; +use PHPStan\Reflection\ReflectionProviderStaticAccessor; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Generic\TemplateTypeHelper; +use ReflectionIntersectionType; use ReflectionNamedType; use ReflectionType; use ReflectionUnionType; +use function array_map; +use function count; +use function get_class; +use function sprintf; +use function str_ends_with; +use function strtolower; class TypehintHelper { @@ -40,28 +48,36 @@ private static function getTypeObjectFromTypehint(string $typeString, ?string $s case 'self': return $selfClass !== null ? new ObjectType($selfClass) : new ErrorType(); case 'parent': - $broker = Broker::getInstance(); - if ($selfClass !== null && $broker->hasClass($selfClass)) { - $classReflection = $broker->getClass($selfClass); - if ($classReflection->getParentClass() !== false) { + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + if ($selfClass !== null && $reflectionProvider->hasClass($selfClass)) { + $classReflection = $reflectionProvider->getClass($selfClass); + if ($classReflection->getParentClass() !== null) { return new ObjectType($classReflection->getParentClass()->getName()); } } return new NonexistentParentClassType(); case 'static': - return $selfClass !== null ? new StaticType($selfClass) : new ErrorType(); + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); + if ($selfClass !== null && $reflectionProvider->hasClass($selfClass)) { + return new StaticType($reflectionProvider->getClass($selfClass)); + } + + return new ErrorType(); case 'null': return new NullType(); + case 'never': + return new NeverType(true); default: return new ObjectType($typeString); } } + /** @api */ public static function decideTypeFromReflection( - ?\ReflectionType $reflectionType, + ?ReflectionType $reflectionType, ?Type $phpDocType = null, ?string $selfClass = null, - bool $isVariadic = false + bool $isVariadic = false, ): Type { if ($reflectionType === null) { @@ -72,30 +88,45 @@ public static function decideTypeFromReflection( } if ($reflectionType instanceof ReflectionUnionType) { - $type = TypeCombinator::union(...array_map(static function (ReflectionType $type) use ($selfClass): Type { - return self::decideTypeFromReflection($type, null, $selfClass, false); - }, $reflectionType->getTypes())); + $type = TypeCombinator::union(...array_map(static fn (ReflectionType $type): Type => self::decideTypeFromReflection($type, null, $selfClass, false), $reflectionType->getTypes())); return self::decideType($type, $phpDocType); } + if ($reflectionType instanceof ReflectionIntersectionType) { + $types = []; + foreach ($reflectionType->getTypes() as $innerReflectionType) { + $innerType = self::decideTypeFromReflection($innerReflectionType, null, $selfClass, false); + if (!$innerType instanceof ObjectType) { + return new NeverType(); + } + + $types[] = $innerType; + } + + return self::decideType(TypeCombinator::intersect(...$types), $phpDocType); + } + if (!$reflectionType instanceof ReflectionNamedType) { - throw new \PHPStan\ShouldNotHappenException(sprintf('Unexpected type: %s', get_class($reflectionType))); + throw new ShouldNotHappenException(sprintf('Unexpected type: %s', get_class($reflectionType))); } $reflectionTypeString = $reflectionType->getName(); - if (\Nette\Utils\Strings::endsWith(strtolower($reflectionTypeString), '\\object')) { + if (str_ends_with(strtolower($reflectionTypeString), '\\object')) { $reflectionTypeString = 'object'; } - if (\Nette\Utils\Strings::endsWith(strtolower($reflectionTypeString), '\\mixed')) { + if (str_ends_with(strtolower($reflectionTypeString), '\\mixed')) { $reflectionTypeString = 'mixed'; } - if (\Nette\Utils\Strings::endsWith(strtolower($reflectionTypeString), '\\false')) { + if (str_ends_with(strtolower($reflectionTypeString), '\\false')) { $reflectionTypeString = 'false'; } - if (\Nette\Utils\Strings::endsWith(strtolower($reflectionTypeString), '\\null')) { + if (str_ends_with(strtolower($reflectionTypeString), '\\null')) { $reflectionTypeString = 'null'; } + if (str_ends_with(strtolower($reflectionTypeString), '\\never')) { + $reflectionTypeString = 'never'; + } $type = self::getTypeObjectFromTypehint($reflectionTypeString, $selfClass); if ($reflectionType->allowsNull()) { @@ -109,7 +140,7 @@ public static function decideTypeFromReflection( public static function decideType( Type $type, - ?Type $phpDocType = null + ?Type $phpDocType = null, ): Type { if ($phpDocType !== null && !$phpDocType instanceof ErrorType) { @@ -134,7 +165,7 @@ public static function decideType( if ($innerType instanceof ArrayType) { $innerTypes[] = new IterableType( $innerType->getKeyType(), - $innerType->getItemType() + $innerType->getItemType(), ); } else { $innerTypes[] = $innerType; @@ -144,7 +175,7 @@ public static function decideType( } elseif ($phpDocType instanceof ArrayType) { $phpDocType = new IterableType( $phpDocType->getKeyType(), - $phpDocType->getItemType() + $phpDocType->getItemType(), ); } } diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index a4bdc89bb7..95c12bb756 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -2,26 +2,40 @@ namespace PHPStan\Type; +use DateTime; +use DateTimeImmutable; +use DateTimeInterface; use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\PropertyReflection; use PHPStan\Reflection\Type\UnionTypeUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnionTypeUnresolvedPropertyPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; 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\TemplateType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\Generic\TemplateUnionType; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; +use function array_map; +use function count; +use function implode; +use function sprintf; +use function strpos; /** @api */ class UnionType implements CompoundType { - /** @var \PHPStan\Type\Type[] */ + use NonGeneralizableTypeTrait; + + /** @var Type[] */ private array $types; /** @@ -31,12 +45,10 @@ class UnionType implements CompoundType public function __construct(array $types) { $throwException = static function () use ($types): void { - throw new \PHPStan\ShouldNotHappenException(sprintf( + throw new ShouldNotHappenException(sprintf( 'Cannot create %s with: %s', self::class, - implode(', ', array_map(static function (Type $type): string { - return $type->describe(VerbosityLevel::value()); - }, $types)) + implode(', ', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::value()), $types)), )); }; if (count($types) < 2) { @@ -46,6 +58,9 @@ public function __construct(array $types) if (!($type instanceof UnionType)) { continue; } + if ($type instanceof TemplateType) { + continue; + } $throwException(); } @@ -53,7 +68,7 @@ public function __construct(array $types) } /** - * @return \PHPStan\Type\Type[] + * @return Type[] */ public function getTypes(): array { @@ -70,8 +85,18 @@ public function getReferencedClasses(): array public function accepts(Type $type, bool $strictTypes): TrinaryLogic { - if ($type instanceof CompoundType && !$type instanceof CallableType) { - return CompoundTypeHelper::accepts($type, $this, $strictTypes); + if ( + $type->equals(new ObjectType(DateTimeInterface::class)) + && $this->accepts( + new UnionType([new ObjectType(DateTime::class), new ObjectType(DateTimeImmutable::class)]), + $strictTypes, + )->yes() + ) { + return TrinaryLogic::createYes(); + } + + if ($type instanceof CompoundType && !$type instanceof CallableType && !$type instanceof TemplateType) { + return $type->isAcceptedBy($this, $strictTypes); } $results = []; @@ -79,12 +104,19 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic $results[] = $innerType->accepts($type, $strictTypes); } + if ($type instanceof TemplateUnionType) { + $results[] = $type->isAcceptedBy($this, $strictTypes); + } + return TrinaryLogic::createNo()->or(...$results); } public function isSuperTypeOf(Type $otherType): TrinaryLogic { - if ($otherType instanceof self || $otherType instanceof IterableType) { + if ( + ($otherType instanceof self && !$otherType instanceof TemplateUnionType) + || $otherType instanceof IterableType + ) { return $otherType->isSubTypeOf($this); } @@ -93,6 +125,10 @@ public function isSuperTypeOf(Type $otherType): TrinaryLogic $results[] = $innerType->isSuperTypeOf($otherType); } + if ($otherType instanceof TemplateUnionType) { + $results[] = $otherType->isSubTypeOf($this); + } + return TrinaryLogic::createNo()->or(...$results); } @@ -140,7 +176,7 @@ public function describe(VerbosityLevel $level): string $joinTypes = static function (array $types) use ($level): string { $typeNames = []; foreach ($types as $type) { - if ($type instanceof ClosureType || $type instanceof CallableType) { + if ($type instanceof ClosureType || $type instanceof CallableType || $type instanceof TemplateUnionType) { $typeNames[] = sprintf('(%s)', $type->describe($level)); } elseif ($type instanceof IntersectionType) { $intersectionDescription = $type->describe($level); @@ -164,7 +200,7 @@ function () use ($joinTypes): string { $type instanceof ConstantType && !$type instanceof ConstantBooleanType ) { - return $type->generalize(); + return $type->generalize(GeneralizePrecision::lessSpecific()); } return $type; @@ -176,20 +212,17 @@ function () use ($joinTypes): string { return $joinTypes([$types]); }, - function () use ($joinTypes): string { - return $joinTypes($this->types); - } + fn (): string => $joinTypes($this->types), ); } /** * @param callable(Type $type): TrinaryLogic $canCallback * @param callable(Type $type): TrinaryLogic $hasCallback - * @return TrinaryLogic */ private function hasInternal( callable $canCallback, - callable $hasCallback + callable $hasCallback, ): TrinaryLogic { $results = []; @@ -207,12 +240,11 @@ private function hasInternal( /** * @param callable(Type $type): TrinaryLogic $hasCallback * @param callable(Type $type): object $getCallback - * @return object */ private function getInternal( callable $hasCallback, - callable $getCallback - ) + callable $getCallback, + ): object { /** @var TrinaryLogic|null $result */ $result = null; @@ -234,7 +266,7 @@ private function getInternal( } if ($object === null) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $object; @@ -242,16 +274,12 @@ private function getInternal( public function canAccessProperties(): TrinaryLogic { - return $this->unionResults(static function (Type $type): TrinaryLogic { - return $type->canAccessProperties(); - }); + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->canAccessProperties()); } public function hasProperty(string $propertyName): TrinaryLogic { - return $this->unionResults(static function (Type $type) use ($propertyName): TrinaryLogic { - return $type->hasProperty($propertyName); - }); + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasProperty($propertyName)); } public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection @@ -272,7 +300,7 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember $propertiesCount = count($propertyPrototypes); if ($propertiesCount === 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ($propertiesCount === 1) { @@ -284,16 +312,12 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember public function canCallMethods(): TrinaryLogic { - return $this->unionResults(static function (Type $type): TrinaryLogic { - return $type->canCallMethods(); - }); + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->canCallMethods()); } public function hasMethod(string $methodName): TrinaryLogic { - return $this->unionResults(static function (Type $type) use ($methodName): TrinaryLogic { - return $type->hasMethod($methodName); - }); + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasMethod($methodName)); } public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection @@ -314,7 +338,7 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce $methodsCount = count($methodPrototypes); if ($methodsCount === 0) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } if ($methodsCount === 1) { @@ -326,89 +350,78 @@ public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAcce public function canAccessConstants(): TrinaryLogic { - return $this->unionResults(static function (Type $type): TrinaryLogic { - return $type->canAccessConstants(); - }); + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->canAccessConstants()); } public function hasConstant(string $constantName): TrinaryLogic { return $this->hasInternal( - static function (Type $type): TrinaryLogic { - return $type->canAccessConstants(); - }, - static function (Type $type) use ($constantName): TrinaryLogic { - return $type->hasConstant($constantName); - } + static fn (Type $type): TrinaryLogic => $type->canAccessConstants(), + static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName), ); } public function getConstant(string $constantName): ConstantReflection { return $this->getInternal( - static function (Type $type) use ($constantName): TrinaryLogic { - return $type->hasConstant($constantName); - }, - static function (Type $type) use ($constantName): ConstantReflection { - return $type->getConstant($constantName); - } + static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName), + static fn (Type $type): ConstantReflection => $type->getConstant($constantName), ); } public function isIterable(): TrinaryLogic { - return $this->unionResults(static function (Type $type): TrinaryLogic { - return $type->isIterable(); - }); + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isIterable()); } public function isIterableAtLeastOnce(): TrinaryLogic { - return $this->unionResults(static function (Type $type): TrinaryLogic { - return $type->isIterableAtLeastOnce(); - }); + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isIterableAtLeastOnce()); } public function getIterableKeyType(): Type { - return $this->unionTypes(static function (Type $type): Type { - return $type->getIterableKeyType(); - }); + return $this->unionTypes(static fn (Type $type): Type => $type->getIterableKeyType()); } public function getIterableValueType(): Type { - return $this->unionTypes(static function (Type $type): Type { - return $type->getIterableValueType(); - }); + return $this->unionTypes(static fn (Type $type): Type => $type->getIterableValueType()); } public function isArray(): TrinaryLogic { - return $this->unionResults(static function (Type $type): TrinaryLogic { - return $type->isArray(); - }); + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isArray()); + } + + public function isString(): TrinaryLogic + { + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isString()); } public function isNumericString(): TrinaryLogic { - return $this->unionResults(static function (Type $type): TrinaryLogic { - return $type->isNumericString(); - }); + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isNumericString()); + } + + public function isNonEmptyString(): TrinaryLogic + { + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isNonEmptyString()); + } + + public function isLiteralString(): TrinaryLogic + { + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isLiteralString()); } public function isOffsetAccessible(): TrinaryLogic { - return $this->unionResults(static function (Type $type): TrinaryLogic { - return $type->isOffsetAccessible(); - }); + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessible()); } public function hasOffsetValueType(Type $offsetType): TrinaryLogic { - return $this->unionResults(static function (Type $type) use ($offsetType): TrinaryLogic { - return $type->hasOffsetValueType($offsetType); - }); + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType)); } public function getOffsetValueType(Type $offsetType): Type @@ -432,21 +445,21 @@ public function getOffsetValueType(Type $offsetType): Type public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { - return $this->unionTypes(static function (Type $type) use ($offsetType, $valueType, $unionValues): Type { - return $type->setOffsetValueType($offsetType, $valueType, $unionValues); - }); + return $this->unionTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues)); + } + + public function unsetOffset(Type $offsetType): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->unsetOffset($offsetType)); } public function isCallable(): TrinaryLogic { - return $this->unionResults(static function (Type $type): TrinaryLogic { - return $type->isCallable(); - }); + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isCallable()); } /** - * @param \PHPStan\Reflection\ClassMemberAccessAnswerer $scope - * @return \PHPStan\Reflection\ParametersAcceptor[] + * @return ParametersAcceptor[] */ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array { @@ -458,123 +471,93 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) return $type->getCallableParametersAcceptors($scope); } - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } public function isCloneable(): TrinaryLogic { - return $this->unionResults(static function (Type $type): TrinaryLogic { - return $type->isCloneable(); - }); + return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isCloneable()); } public function isSmallerThan(Type $otherType): TrinaryLogic { - return $this->unionResults(static function (Type $type) use ($otherType): TrinaryLogic { - return $type->isSmallerThan($otherType); - }); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThan($otherType)); } public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic { - return $this->unionResults(static function (Type $type) use ($otherType): TrinaryLogic { - return $type->isSmallerThanOrEqual($otherType); - }); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType)); } public function getSmallerType(): Type { - return $this->unionTypes(static function (Type $type): Type { - return $type->getSmallerType(); - }); + return $this->unionTypes(static fn (Type $type): Type => $type->getSmallerType()); } public function getSmallerOrEqualType(): Type { - return $this->unionTypes(static function (Type $type): Type { - return $type->getSmallerOrEqualType(); - }); + return $this->unionTypes(static fn (Type $type): Type => $type->getSmallerOrEqualType()); } public function getGreaterType(): Type { - return $this->unionTypes(static function (Type $type): Type { - return $type->getGreaterType(); - }); + return $this->unionTypes(static fn (Type $type): Type => $type->getGreaterType()); } public function getGreaterOrEqualType(): Type { - return $this->unionTypes(static function (Type $type): Type { - return $type->getGreaterOrEqualType(); - }); + return $this->unionTypes(static fn (Type $type): Type => $type->getGreaterOrEqualType()); } public function isGreaterThan(Type $otherType): TrinaryLogic { - return $this->unionResults(static function (Type $type) use ($otherType): TrinaryLogic { - return $otherType->isSmallerThan($type); - }); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type)); } public function isGreaterThanOrEqual(Type $otherType): TrinaryLogic { - return $this->unionResults(static function (Type $type) use ($otherType): TrinaryLogic { - return $otherType->isSmallerThanOrEqual($type); - }); + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type)); } public function toBoolean(): BooleanType { /** @var BooleanType $type */ - $type = $this->unionTypes(static function (Type $type): BooleanType { - return $type->toBoolean(); - }); + $type = $this->unionTypes(static fn (Type $type): BooleanType => $type->toBoolean()); return $type; } public function toNumber(): Type { - $type = $this->unionTypes(static function (Type $type): Type { - return $type->toNumber(); - }); + $type = $this->unionTypes(static fn (Type $type): Type => $type->toNumber()); return $type; } public function toString(): Type { - $type = $this->unionTypes(static function (Type $type): Type { - return $type->toString(); - }); + $type = $this->unionTypes(static fn (Type $type): Type => $type->toString()); return $type; } public function toInteger(): Type { - $type = $this->unionTypes(static function (Type $type): Type { - return $type->toInteger(); - }); + $type = $this->unionTypes(static fn (Type $type): Type => $type->toInteger()); return $type; } public function toFloat(): Type { - $type = $this->unionTypes(static function (Type $type): Type { - return $type->toFloat(); - }); + $type = $this->unionTypes(static fn (Type $type): Type => $type->toFloat()); return $type; } public function toArray(): Type { - $type = $this->unionTypes(static function (Type $type): Type { - return $type->toArray(); - }); + $type = $this->unionTypes(static fn (Type $type): Type => $type->toArray()); return $type; } @@ -667,9 +650,13 @@ public function traverse(callable $cb): Type return $this; } + public function tryRemove(Type $typeToRemove): ?Type + { + return $this->unionTypes(static fn (Type $type): Type => TypeCombinator::remove($type, $typeToRemove)); + } + /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { @@ -678,16 +665,22 @@ public static function __set_state(array $properties): Type /** * @param callable(Type $type): TrinaryLogic $getResult - * @return TrinaryLogic */ protected function unionResults(callable $getResult): TrinaryLogic { return TrinaryLogic::extremeIdentity(...array_map($getResult, $this->types)); } + /** + * @param callable(Type $type): TrinaryLogic $getResult + */ + private function notBenevolentUnionResults(callable $getResult): TrinaryLogic + { + return TrinaryLogic::extremeIdentity(...array_map($getResult, $this->types)); + } + /** * @param callable(Type $type): Type $getType - * @return Type */ protected function unionTypes(callable $getType): Type { diff --git a/src/Type/UnionTypeHelper.php b/src/Type/UnionTypeHelper.php index c0c99bbd18..68f9d1c534 100644 --- a/src/Type/UnionTypeHelper.php +++ b/src/Type/UnionTypeHelper.php @@ -8,12 +8,17 @@ use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use function array_merge; +use function count; +use function strcasecmp; +use function usort; +use const PHP_INT_MIN; class UnionTypeHelper { /** - * @param \PHPStan\Type\Type[] $types + * @param Type[] $types * @return string[] */ public static function getReferencedClasses(array $types): array @@ -27,11 +32,15 @@ public static function getReferencedClasses(array $types): array } /** - * @param \PHPStan\Type\Type[] $types - * @return \PHPStan\Type\Type[] + * @param Type[] $types + * @return Type[] */ public static function sortTypes(array $types): array { + if (count($types) > 1024) { + return $types; + } + usort($types, static function (Type $a, Type $b): int { if ($a instanceof NullType) { return 1; @@ -90,6 +99,14 @@ public static function sortTypes(array $types): array return ($a->getMin() ?? PHP_INT_MIN) <=> ($b->getMin() ?? PHP_INT_MIN); } + if ($a instanceof IntegerRangeType && $b instanceof IntegerType) { + return 1; + } + + if ($b instanceof IntegerRangeType && $a instanceof IntegerType) { + return -1; + } + if ($a instanceof ConstantStringType && $b instanceof ConstantStringType) { return strcasecmp($a->getValue(), $b->getValue()); } diff --git a/src/Type/UsefulTypeAliasResolver.php b/src/Type/UsefulTypeAliasResolver.php new file mode 100644 index 0000000000..6cfb76d373 --- /dev/null +++ b/src/Type/UsefulTypeAliasResolver.php @@ -0,0 +1,153 @@ + */ + private array $resolvedGlobalTypeAliases = []; + + /** @var array */ + private array $resolvedLocalTypeAliases = []; + + /** @var array */ + private array $resolvingClassTypeAliases = []; + + /** @var array */ + private array $inProcess = []; + + /** + * @param array $globalTypeAliases + */ + public function __construct( + private array $globalTypeAliases, + private TypeStringResolver $typeStringResolver, + private TypeNodeResolver $typeNodeResolver, + private ReflectionProvider $reflectionProvider, + ) + { + } + + public function hasTypeAlias(string $aliasName, ?string $classNameScope): bool + { + $hasGlobalTypeAlias = array_key_exists($aliasName, $this->globalTypeAliases); + if ($hasGlobalTypeAlias) { + return true; + } + + if ($classNameScope === null || !$this->reflectionProvider->hasClass($classNameScope)) { + return false; + } + + $classReflection = $this->reflectionProvider->getClass($classNameScope); + $localTypeAliases = $classReflection->getTypeAliases(); + return array_key_exists($aliasName, $localTypeAliases); + } + + public function resolveTypeAlias(string $aliasName, NameScope $nameScope): ?Type + { + return $this->resolveLocalTypeAlias($aliasName, $nameScope) + ?? $this->resolveGlobalTypeAlias($aliasName, $nameScope); + } + + private function resolveLocalTypeAlias(string $aliasName, NameScope $nameScope): ?Type + { + if (array_key_exists($aliasName, $this->globalTypeAliases)) { + return null; + } + + if (!$nameScope->hasTypeAlias($aliasName)) { + return null; + } + + $className = $nameScope->getClassName(); + if ($className === null) { + return null; + } + + $aliasNameInClassScope = $className . '::' . $aliasName; + + if (array_key_exists($aliasNameInClassScope, $this->resolvedLocalTypeAliases)) { + return $this->resolvedLocalTypeAliases[$aliasNameInClassScope]; + } + + // prevent infinite recursion + if (array_key_exists($className, $this->resolvingClassTypeAliases)) { + return null; + } + + $this->resolvingClassTypeAliases[$className] = true; + + if (!$this->reflectionProvider->hasClass($className)) { + unset($this->resolvingClassTypeAliases[$className]); + return null; + } + + $classReflection = $this->reflectionProvider->getClass($className); + $localTypeAliases = $classReflection->getTypeAliases(); + + unset($this->resolvingClassTypeAliases[$className]); + + if (!array_key_exists($aliasName, $localTypeAliases)) { + return null; + } + + if (array_key_exists($aliasNameInClassScope, $this->inProcess)) { + // resolve circular reference as ErrorType to make it easier to detect + throw new CircularTypeAliasDefinitionException(); + } + + $this->inProcess[$aliasNameInClassScope] = true; + + try { + $unresolvedAlias = $localTypeAliases[$aliasName]; + $resolvedAliasType = $unresolvedAlias->resolve($this->typeNodeResolver); + } catch (CircularTypeAliasDefinitionException) { + $resolvedAliasType = new CircularTypeAliasErrorType(); + } + + $this->resolvedLocalTypeAliases[$aliasNameInClassScope] = $resolvedAliasType; + unset($this->inProcess[$aliasNameInClassScope]); + + return $resolvedAliasType; + } + + private function resolveGlobalTypeAlias(string $aliasName, NameScope $nameScope): ?Type + { + if (!array_key_exists($aliasName, $this->globalTypeAliases)) { + return null; + } + + if (array_key_exists($aliasName, $this->resolvedGlobalTypeAliases)) { + return $this->resolvedGlobalTypeAliases[$aliasName]; + } + + if ($this->reflectionProvider->hasClass($nameScope->resolveStringName($aliasName))) { + throw new ShouldNotHappenException(sprintf('Type alias %s already exists as a class.', $aliasName)); + } + + if (array_key_exists($aliasName, $this->inProcess)) { + throw new ShouldNotHappenException(sprintf('Circular definition for type alias %s.', $aliasName)); + } + + $this->inProcess[$aliasName] = true; + + $aliasTypeString = $this->globalTypeAliases[$aliasName]; + $aliasType = $this->typeStringResolver->resolve($aliasTypeString); + $this->resolvedGlobalTypeAliases[$aliasName] = $aliasType; + + unset($this->inProcess[$aliasName]); + + return $aliasType; + } + +} diff --git a/src/Type/VerbosityLevel.php b/src/Type/VerbosityLevel.php index 87b323e1d1..fe3bfc0610 100644 --- a/src/Type/VerbosityLevel.php +++ b/src/Type/VerbosityLevel.php @@ -2,6 +2,9 @@ namespace PHPStan\Type; +use PHPStan\ShouldNotHappenException; +use PHPStan\Type\Accessory\AccessoryLiteralStringType; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\Generic\GenericObjectType; @@ -18,16 +21,13 @@ class VerbosityLevel /** @var self[] */ private static array $registry; - private int $value; - - private function __construct(int $value) + private function __construct(private int $value) { - $this->value = $value; } private static function create(int $value): self { - self::$registry[$value] = self::$registry[$value] ?? new self($value); + self::$registry[$value] ??= new self($value); return self::$registry[$value]; } @@ -55,6 +55,11 @@ public static function cache(): self return self::create(self::CACHE); } + public function isTypeOnly(): bool + { + return $this->value === self::TYPE_ONLY; + } + /** @api */ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acceptedType = null): self { @@ -67,11 +72,13 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc $moreVerbose = true; return $type; } - if ($type instanceof AccessoryNumericStringType) { - $moreVerbose = true; - return $type; - } - if ($type instanceof NonEmptyArrayType) { + if ( + // synced with IntersectionType::describe() + $type instanceof AccessoryNonEmptyStringType + || $type instanceof AccessoryLiteralStringType + || $type instanceof AccessoryNumericStringType + || $type instanceof NonEmptyArrayType + ) { $moreVerbose = true; return $type; } @@ -134,13 +141,12 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc * @param callable(): string $valueCallback * @param callable(): string|null $preciseCallback * @param callable(): string|null $cacheCallback - * @return string */ public function handle( callable $typeOnlyCallback, callable $valueCallback, ?callable $preciseCallback = null, - ?callable $cacheCallback = null + ?callable $cacheCallback = null, ): string { if ($this->value === self::TYPE_ONLY) { @@ -171,7 +177,7 @@ public function handle( return $valueCallback(); } - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } } diff --git a/src/Type/VoidType.php b/src/Type/VoidType.php index 61706e5e08..db99d85e7e 100644 --- a/src/Type/VoidType.php +++ b/src/Type/VoidType.php @@ -5,10 +5,12 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Traits\FalseyBooleanTypeTrait; use PHPStan\Type\Traits\NonCallableTypeTrait; +use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\NonIterableTypeTrait; use PHPStan\Type\Traits\NonObjectTypeTrait; use PHPStan\Type\Traits\NonOffsetAccessibleTypeTrait; +use PHPStan\Type\Traits\NonRemoveableTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; /** @api */ @@ -22,6 +24,8 @@ class VoidType implements Type use FalseyBooleanTypeTrait; use NonGenericTypeTrait; use UndecidedComparisonTypeTrait; + use NonRemoveableTypeTrait; + use NonGeneralizableTypeTrait; /** @api */ public function __construct() @@ -98,11 +102,26 @@ public function isArray(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNumericString(): TrinaryLogic { return TrinaryLogic::createNo(); } + public function isNonEmptyString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function traverse(callable $cb): Type { return $this; @@ -110,7 +129,6 @@ public function traverse(callable $cb): Type /** * @param mixed[] $properties - * @return Type */ public static function __set_state(array $properties): Type { diff --git a/stubs/ArrayObject.stub b/stubs/ArrayObject.stub index ac5766994f..37714725f4 100644 --- a/stubs/ArrayObject.stub +++ b/stubs/ArrayObject.stub @@ -88,9 +88,9 @@ class ArrayObject implements IteratorAggregate, ArrayAccess /** * @template TValue - * @implements Iterator - * @implements IteratorAggregate - * @implements ArrayAccess + * @implements Iterator + * @implements IteratorAggregate + * @implements ArrayAccess */ class SplFixedArray implements Iterator, IteratorAggregate, ArrayAccess, Countable { @@ -102,7 +102,7 @@ class SplFixedArray implements Iterator, IteratorAggregate, ArrayAccess, Countab public static function fromArray(array $array, bool $save_indexes = true): SplFixedArray { } /** - * @return array + * @return array */ public function toArray(): array { } } diff --git a/stubs/Exception.stub b/stubs/Exception.stub new file mode 100644 index 0000000000..078b13606b --- /dev/null +++ b/stubs/Exception.stub @@ -0,0 +1,145 @@ + + * @throws void + */ + public function getTrace(); + + /** + * @return string + * @throws void + */ + public function getTraceAsString(); + + /** + * @return null|Throwable + * @throws void + */ + public function getPrevious(); + + /** + * @return string + */ + public function __toString(); +} + +class Exception implements Throwable +{ + + /** + * @return string + * @throws void + */ + final public function getMessage(): string {} + + /** + * @return mixed + * @throws void + */ + final public function getCode() {} + + /** + * @return string + * @throws void + */ + final public function getFile(): string {} + + /** + * @return int + * @throws void + */ + final public function getLine(): int {} + + /** + * @return list + * @throws void + */ + final public function getTrace(): array {} + + /** + * @return null|Throwable + * @throws void + */ + final public function getPrevious(): ?Throwable {} + + /** + * @return string + * @throws void + */ + final public function getTraceAsString(): string {} + +} + +class Error implements Throwable +{ + + /** + * @return string + * @throws void + */ + final public function getMessage(): string {} + + /** + * @return mixed + * @throws void + */ + final public function getCode() {} + + /** + * @return string + * @throws void + */ + final public function getFile(): string {} + + /** + * @return int + * @throws void + */ + final public function getLine(): int {} + + /** + * @return list + * @throws void + */ + final public function getTrace(): array {} + + /** + * @return null|Throwable + * @throws void + */ + final public function getPrevious(): ?Throwable {} + + /** + * @return string + * @throws void + */ + final public function getTraceAsString(): string {} + +} diff --git a/stubs/PDOStatement.stub b/stubs/PDOStatement.stub index 5c63cbce7d..79637d8370 100644 --- a/stubs/PDOStatement.stub +++ b/stubs/PDOStatement.stub @@ -2,9 +2,10 @@ /** * @implements Traversable> + * @implements IteratorAggregate> * @link https://php.net/manual/en/class.pdostatement.php */ -class PDOStatement implements Traversable +class PDOStatement implements Traversable, IteratorAggregate { } diff --git a/stubs/ReflectionAttribute.stub b/stubs/ReflectionAttribute.stub new file mode 100644 index 0000000000..0bac59de5b --- /dev/null +++ b/stubs/ReflectionAttribute.stub @@ -0,0 +1,21 @@ + + */ + public function getName() : string + { + } + + /** + * @return T + */ + public function newInstance() : object + { + } +} diff --git a/stubs/ReflectionClass.stub b/stubs/ReflectionClass.stub index 09c428b0dc..e5d2a0908a 100644 --- a/stubs/ReflectionClass.stub +++ b/stubs/ReflectionClass.stub @@ -42,4 +42,10 @@ class ReflectionClass */ public function newInstanceWithoutConstructor(); + /** + * @return array> + */ + public function getAttributes(?string $name = null, int $flags = 0) + { + } } diff --git a/stubs/ReflectionClassConstant.stub b/stubs/ReflectionClassConstant.stub new file mode 100644 index 0000000000..669ccbef89 --- /dev/null +++ b/stubs/ReflectionClassConstant.stub @@ -0,0 +1,11 @@ +> + */ + public function getAttributes(?string $name = null, int $flags = 0) + { + } +} diff --git a/stubs/ReflectionFunctionAbstract.stub b/stubs/ReflectionFunctionAbstract.stub index c096d0f608..36e48df100 100644 --- a/stubs/ReflectionFunctionAbstract.stub +++ b/stubs/ReflectionFunctionAbstract.stub @@ -8,4 +8,10 @@ abstract class ReflectionFunctionAbstract */ public function getFileName () {} + /** + * @return array> + */ + public function getAttributes(?string $name = null, int $flags = 0) + { + } } diff --git a/stubs/ReflectionParameter.stub b/stubs/ReflectionParameter.stub new file mode 100644 index 0000000000..a29fa35e83 --- /dev/null +++ b/stubs/ReflectionParameter.stub @@ -0,0 +1,11 @@ +> + */ + public function getAttributes(?string $name = null, int $flags = 0) + { + } +} diff --git a/stubs/ReflectionProperty.stub b/stubs/ReflectionProperty.stub new file mode 100644 index 0000000000..312688067d --- /dev/null +++ b/stubs/ReflectionProperty.stub @@ -0,0 +1,11 @@ +> + */ + public function getAttributes(?string $name = null, int $flags = 0) + { + } +} diff --git a/stubs/WeakReference.stub b/stubs/WeakReference.stub index 7a71b77065..5f23dbca9c 100644 --- a/stubs/WeakReference.stub +++ b/stubs/WeakReference.stub @@ -16,3 +16,14 @@ final class WeakReference /** @return ?T */ public function get() {} } + + +/** + * @template TKey of object + * @template TValue + * @implements \ArrayAccess + * @implements \IteratorAggregate + */ +final class WeakMap implements \ArrayAccess, \Countable, \IteratorAggregate +{ +} diff --git a/stubs/bleedingEdge/Countable.stub b/stubs/bleedingEdge/Countable.stub new file mode 100644 index 0000000000..2491db1ce5 --- /dev/null +++ b/stubs/bleedingEdge/Countable.stub @@ -0,0 +1,9 @@ + + * @implements \Traversable + */ +class DatePeriod implements \IteratorAggregate, \Traversable +{ + + /** + * @return TEnd + */ + public function getEndDate() + { + + } + + /** + * @return TRecurrences + */ + public function getRecurrences() + { + + } + + /** + * @return TDate + */ + public function getStartDate(): DateTimeInterface + { + + } +} diff --git a/stubs/ext-ds.stub b/stubs/ext-ds.stub index 99066ac971..1075d9a31d 100644 --- a/stubs/ext-ds.stub +++ b/stubs/ext-ds.stub @@ -2,6 +2,7 @@ namespace Ds; +use ArrayAccess; use Countable; use JsonSerializable; use OutOfBoundsException; @@ -19,7 +20,7 @@ interface Collection extends Traversable, Countable, JsonSerializable /** * @return Collection */ - public function copy(): Collection; + public function copy(); /** * @return array @@ -43,7 +44,7 @@ final class Deque implements Sequence /** * @return Deque */ - public function copy(): Deque + public function copy() { } @@ -92,8 +93,9 @@ final class Deque implements Sequence * @template TKey * @template TValue * @implements Collection + * @implements ArrayAccess */ -final class Map implements Collection +final class Map implements Collection, ArrayAccess { /** * @param iterable $values @@ -380,8 +382,9 @@ final class Pair implements JsonSerializable /** * @template TValue * @extends Collection + * @extends ArrayAccess */ -interface Sequence extends Collection +interface Sequence extends Collection, ArrayAccess { /** * @param callable(TValue): TValue $callback @@ -389,6 +392,11 @@ interface Sequence extends Collection */ public function apply(callable $callback); + /** + * @return Sequence + */ + public function copy(); + /** * @param TValue ...$values */ @@ -398,7 +406,7 @@ interface Sequence extends Collection * @param (callable(TValue): bool)|null $callback * @return Sequence */ - public function filter(callable $callback = null): Sequence; + public function filter(callable $callback = null); /** * @param TValue $value @@ -442,14 +450,14 @@ interface Sequence extends Collection * @param callable(TValue): TNewValue $callback * @return Sequence */ - public function map(callable $callback): Sequence; + public function map(callable $callback); /** * @template TValue2 * @param iterable $values * @return Sequence */ - public function merge(iterable $values): Sequence; + public function merge(iterable $values); /** * @return TValue @@ -480,7 +488,7 @@ interface Sequence extends Collection /** * @return Sequence */ - public function reversed(): Sequence; + public function reversed(); /** * @param TValue $value @@ -498,7 +506,7 @@ interface Sequence extends Collection /** * @return Sequence */ - public function slice(int $index, ?int $length = null): Sequence; + public function slice(int $index, ?int $length = null); /** * @param (callable(TValue, TValue): int)|null $comparator @@ -510,7 +518,7 @@ interface Sequence extends Collection * @param (callable(TValue, TValue): int)|null $comparator * @return Sequence */ - public function sorted(callable $comparator = null): Sequence; + public function sorted(callable $comparator = null); /** * @param TValue ...$values @@ -536,7 +544,7 @@ final class Vector implements Sequence /** * @return Vector */ - public function copy(): Vector + public function copy() { } @@ -592,8 +600,9 @@ final class Vector implements Sequence /** * @template TValue * @implements Collection + * @implements ArrayAccess */ -final class Set implements Collection +final class Set implements Collection, ArrayAccess { /** * @param iterable $values @@ -673,6 +682,15 @@ final class Set implements Collection { } + /** + * @template TNewValue + * @param callable(TValue): TNewValue $callback + * @return Set + */ + public function map(callable $callback): Set + { + } + /** * @template TValue2 * @param iterable $values @@ -682,6 +700,16 @@ final class Set implements Collection { } + /** + * @template TCarry + * @param callable(TCarry, TValue): TCarry $callback + * @param TCarry $initial + * @return TCarry + */ + public function reduce(callable $callback, $initial = null) + { + } + /** * @param TValue ...$values */ @@ -747,8 +775,9 @@ final class Set implements Collection /** * @template TValue * @implements Collection + * @implements ArrayAccess */ -final class Stack implements Collection +final class Stack implements Collection, ArrayAccess { /** * @param iterable $values @@ -799,8 +828,9 @@ final class Stack implements Collection /** * @template TValue * @implements Collection + * @implements ArrayAccess */ -final class Queue implements Collection +final class Queue implements Collection, ArrayAccess { /** * @param iterable $values diff --git a/stubs/iterable.stub b/stubs/iterable.stub index cbcba7b8a5..04040e735f 100644 --- a/stubs/iterable.stub +++ b/stubs/iterable.stub @@ -81,10 +81,10 @@ class Generator implements Iterator } /** - * @implements Traversable - * @implements ArrayAccess - * @implements Iterator - * @implements RecursiveIterator + * @implements Traversable + * @implements ArrayAccess + * @implements Iterator + * @implements RecursiveIterator */ class SimpleXMLElement implements Traversable, ArrayAccess, Iterator, RecursiveIterator { @@ -193,6 +193,53 @@ class IteratorIterator implements OuterIterator { public function __construct(Traversable $iterator) {} } +/** + * @template-covariant TKey + * @template-covariant TValue + * @template TIterator as Traversable + * + * @template-extends IteratorIterator + */ +class FilterIterator extends IteratorIterator +{ + +} + +/** + * @template-covariant TKey + * @template-covariant TValue + * @template TIterator as Traversable + * + * @extends FilterIterator + */ +class CallbackFilterIterator extends FilterIterator +{ + +} + + +/** + * @template-covariant TKey + * @template-covariant TValue + * @template TIterator as Traversable + * + * @extends CallbackFilterIterator + * @implements RecursiveIterator + */ +class RecursiveCallbackFilterIterator extends CallbackFilterIterator implements RecursiveIterator +{ + /** + * @return bool + */ + public function hasChildren() {} + + /** + * @return RecursiveCallbackFilterIterator + */ + public function getChildren() {} + +} + /** * @template TKey of array-key * @template TValue diff --git a/stubs/mysqli.stub b/stubs/mysqli.stub new file mode 100644 index 0000000000..350a645ba3 --- /dev/null +++ b/stubs/mysqli.stub @@ -0,0 +1,51 @@ + + */ + public $error_list; + + /** + * @var string + */ + public $error; + + /** + * @var 0|positive-int + */ + public $field_count; + + /** + * @var int|string + */ + public $insert_id; + + /** + * @var 0|positive-int + */ + public $num_rows; + + /** + * @var 0|positive-int + */ + public $param_count; + + /** + * @var non-empty-string + */ + public $sqlstate; + +} diff --git a/stubs/runtime/Attribute.php b/stubs/runtime/Attribute.php index d248c52cfe..e41f42cfc4 100644 --- a/stubs/runtime/Attribute.php +++ b/stubs/runtime/Attribute.php @@ -1,10 +1,6 @@ runAnalyse(__DIR__ . '/../../notAutoloaded/Foo.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testMissingClassErrorAboutMisconfiguredAutoloader(): void { $errors = $this->runAnalyse(__DIR__ . '/../../notAutoloaded/Bar.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testMissingFunctionErrorAboutMisconfiguredAutoloader(): void @@ -52,7 +56,7 @@ public function testMissingFunctionErrorAboutMisconfiguredAutoloader(): void public function testAnonymousClassWithInheritedConstructor(): void { $errors = $this->runAnalyse(__DIR__ . '/data/anonymous-class-with-inherited-constructor.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testNestedFunctionCallsDoNotCauseExcessiveFunctionNesting(): void @@ -61,7 +65,7 @@ public function testNestedFunctionCallsDoNotCauseExcessiveFunctionNesting(): voi $this->markTestSkipped('This test takes too long with XDebug enabled.'); } $errors = $this->runAnalyse(__DIR__ . '/data/nested-functions.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testExtendingUnknownClass(): void @@ -81,16 +85,16 @@ public function testExtendingUnknownClass(): void public function testExtendingKnownClassWithCheck(): void { $errors = $this->runAnalyse(__DIR__ . '/data/extending-known-class-with-check.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); - $broker = self::getContainer()->getByType(Broker::class); - $this->assertTrue($broker->hasClass(\ExtendingKnownClassWithCheck\Foo::class)); + $reflectionProvider = $this->createReflectionProvider(); + $this->assertTrue($reflectionProvider->hasClass(Foo::class)); } public function testInfiniteRecursionWithCallable(): void { $errors = $this->runAnalyse(__DIR__ . '/data/Foo-callable.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testClassThatExtendsUnknownClassIn3rdPartyPropertyTypeShouldNotCauseAutoloading(): void @@ -127,16 +131,18 @@ public function testCustomFunctionWithNameEquivalentInSignatureMap(): void } require_once __DIR__ . '/data/custom-function-in-signature-map.php'; $errors = $this->runAnalyse(__DIR__ . '/data/custom-function-in-signature-map.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testAnonymousClassWithWrongFilename(): void { $errors = $this->runAnalyse(__DIR__ . '/data/anonymous-class-wrong-filename-regression.php'); $this->assertCount(5, $errors); - $this->assertStringContainsString('Return typehint of method', $errors[0]->getMessage()); + $this->assertStringContainsString('Method', $errors[0]->getMessage()); + $this->assertStringContainsString('has invalid return type', $errors[0]->getMessage()); $this->assertSame(16, $errors[0]->getLine()); - $this->assertStringContainsString('Return typehint of method', $errors[1]->getMessage()); + $this->assertStringContainsString('Method', $errors[1]->getMessage()); + $this->assertStringContainsString('has invalid return type', $errors[1]->getMessage()); $this->assertSame(16, $errors[1]->getLine()); $this->assertSame('Instantiated class AnonymousClassWrongFilename\Bar not found.', $errors[2]->getMessage()); $this->assertSame(18, $errors[2]->getLine()); @@ -153,13 +159,13 @@ public function testExtendsPdoStatementCrash(): void $this->markTestSkipped(); } $errors = $this->runAnalyse(__DIR__ . '/data/extends-pdo-statement.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testArrayDestructuringArrayDimFetch(): void { $errors = $this->runAnalyse(__DIR__ . '/data/array-destructuring-array-dim-fetch.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testNestedNamespaces(): void @@ -168,14 +174,14 @@ public function testNestedNamespaces(): void $this->assertCount(2, $errors); $this->assertSame('Property y\x::$baz has unknown class x\baz as its type.', $errors[0]->getMessage()); $this->assertSame(15, $errors[0]->getLine()); - $this->assertSame('Parameter $baz of method y\x::__construct() has invalid typehint type x\baz.', $errors[1]->getMessage()); + $this->assertSame('Parameter $baz of method y\x::__construct() has invalid type x\baz.', $errors[1]->getMessage()); $this->assertSame(16, $errors[1]->getLine()); } public function testClassExistsAutoloadingError(): void { $errors = $this->runAnalyse(__DIR__ . '/data/class-exists.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testCollectWarnings(): void @@ -207,19 +213,20 @@ public function testCollectWarnings(): void public function testPropertyAssignIntersectionStaticTypeBug(): void { $errors = $this->runAnalyse(__DIR__ . '/data/property-assign-intersection-static-type-bug.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testBug2823(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-2823.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testTwoSameClassesInSingleFile(): void { $errors = $this->runAnalyse(__DIR__ . '/data/two-same-classes.php'); - $this->assertCount(4, $errors); + $this->assertCount(5, $errors); + $error = $errors[0]; $this->assertSame('Property TwoSame\Foo::$prop (string) does not accept default value of type int.', $error->getMessage()); $this->assertSame(9, $error->getLine()); @@ -229,42 +236,46 @@ public function testTwoSameClassesInSingleFile(): void $this->assertSame(13, $error->getLine()); $error = $errors[2]; - $this->assertSame('Property TwoSame\Foo::$prop (int) does not accept default value of type string.', $error->getMessage()); - $this->assertSame(25, $error->getLine()); + $this->assertSame('If condition is always false.', $error->getMessage()); + $this->assertSame(26, $error->getLine()); $error = $errors[3]; + $this->assertSame('Property TwoSame\Foo::$prop (int) does not accept default value of type string.', $error->getMessage()); + $this->assertSame(33, $error->getLine()); + + $error = $errors[4]; $this->assertSame('Property TwoSame\Foo::$prop2 (int) does not accept default value of type string.', $error->getMessage()); - $this->assertSame(28, $error->getLine()); + $this->assertSame(36, $error->getLine()); } public function testBug3405(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-3405.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testBug3415(): void { $errors = $this->runAnalyse(__DIR__ . '/../Rules/Methods/data/bug-3415.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testBug3415Two(): void { $errors = $this->runAnalyse(__DIR__ . '/../Rules/Methods/data/bug-3415-2.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testBug3468(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-3468.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testBug3686(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-3686.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testBug3379(): void @@ -273,27 +284,26 @@ public function testBug3379(): void $this->markTestSkipped('Test requires static reflection'); } $errors = $this->runAnalyse(__DIR__ . '/data/bug-3379.php'); - $this->assertCount(2, $errors); + $this->assertCount(1, $errors); $this->assertSame('Constant SOME_UNKNOWN_CONST not found.', $errors[0]->getMessage()); - $this->assertSame('Reflection error: Could not locate constant "SOME_UNKNOWN_CONST" while evaluating expression in Bug3379\Foo at line 8', $errors[1]->getMessage()); } public function testBug3798(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-3798.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testBug3909(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-3909.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testBug4097(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-4097.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testBug4300(): void @@ -307,46 +317,47 @@ public function testBug4300(): void public function testBug4513(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-4513.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testBug1871(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-1871.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testBug3309(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-3309.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testBug3769(): void { require_once __DIR__ . '/../Rules/Generics/data/bug-3769.php'; $errors = $this->runAnalyse(__DIR__ . '/../Rules/Generics/data/bug-3769.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testBug3922(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-3922-integration.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testBug1843(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-1843.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testBug4713(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-4713.php'); - $this->assertCount(0, $errors); + $this->assertCount(1, $errors); + $this->assertSame('Method Bug4713\Service::createInstance() should return Bug4713\Service but returns object.', $errors[0]->getMessage()); - $reflectionProvider = $this->createBroker(); + $reflectionProvider = $this->createReflectionProvider(); $class = $reflectionProvider->getClass(Service::class); $parameter = ParametersAcceptorSelector::selectSingle($class->getNativeMethod('createInstance')->getVariants())->getParameters()[0]; $defaultValue = $parameter->getDefaultValue(); @@ -357,9 +368,9 @@ public function testBug4713(): void public function testBug4288(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-4288.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); - $reflectionProvider = $this->createBroker(); + $reflectionProvider = $this->createReflectionProvider(); $class = $reflectionProvider->getClass(MyClass::class); $parameter = ParametersAcceptorSelector::selectSingle($class->getNativeMethod('paginate')->getVariants())->getParameters()[0]; $defaultValue = $parameter->getDefaultValue(); @@ -377,14 +388,14 @@ public function testBug4288(): void public function testBug4702(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-4702.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testFunctionThatExistsOn72AndLater(): void { $errors = $this->runAnalyse(__DIR__ . '/data/ldap-exop-passwd.php'); if (PHP_VERSION_ID >= 70200) { - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); return; } @@ -398,31 +409,179 @@ public function testBug4715(): void $this->markTestSkipped('Test requires PHP 7.4.'); } $errors = $this->runAnalyse(__DIR__ . '/data/bug-4715.php'); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testBug4734(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-4734.php'); + $this->assertCount(3, $errors); + + $this->assertSame('Unsafe access to private property Bug4734\Foo::$httpMethodParameterOverride through static::.', $errors[0]->getMessage()); + $this->assertSame('Access to an undefined static property static(Bug4734\Foo)::$httpMethodParameterOverride3.', $errors[1]->getMessage()); + $this->assertSame('Access to an undefined property Bug4734\Foo::$httpMethodParameterOverride4.', $errors[2]->getMessage()); + } + + public function testBug5231(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-5231.php'); + $this->assertNotEmpty($errors); + } + + public function testBug5231Two(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-5231_2.php'); + $this->assertNotEmpty($errors); + } + + public function testBug5529(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-5529.php'); + $this->assertNoErrors($errors); + } + + public function testBug5527(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-5527.php'); + $this->assertNoErrors($errors); + } + + public function testBug5639(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-5639.php'); + $this->assertNoErrors($errors); + } + + public function testBug5657(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-5657.php'); + $this->assertNoErrors($errors); + } + + public function testBug5951(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + $errors = $this->runAnalyse(__DIR__ . '/data/bug-5951.php'); + $this->assertNoErrors($errors); + } + + public function testEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/enums-integration.php'); + $this->assertCount(3, $errors); + $this->assertSame('Access to an undefined property EnumIntegrationTest\Foo::TWO::$value.', $errors[0]->getMessage()); + $this->assertSame(22, $errors[0]->getLine()); + $this->assertSame('Access to undefined constant EnumIntegrationTest\Bar::NONEXISTENT.', $errors[1]->getMessage()); + $this->assertSame(49, $errors[1]->getLine()); + $this->assertSame('Strict comparison using === between EnumIntegrationTest\Foo::ONE and EnumIntegrationTest\Foo::TWO will always evaluate to false.', $errors[2]->getMessage()); + $this->assertSame(79, $errors[2]->getLine()); + } + + public function testBug6255(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6255.php'); + $this->assertNoErrors($errors); + } + + public function testBug6300(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6300.php'); + $this->assertCount(2, $errors); + $this->assertSame('Call to an undefined method Bug6300\Bar::get().', $errors[0]->getMessage()); + $this->assertSame(23, $errors[0]->getLine()); + + $this->assertSame('Access to an undefined property Bug6300\Bar::$fooProp.', $errors[1]->getMessage()); + $this->assertSame(24, $errors[1]->getLine()); + } + + public function testBug6466(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6466.php'); + $this->assertNoErrors($errors); + } + + public function testBug6253(): void + { + $errors = $this->runAnalyse( + __DIR__ . '/data/bug-6253.php', + [ + __DIR__ . '/data/bug-6253.php', + __DIR__ . '/data/bug-6253-app-scope-trait.php', + __DIR__ . '/data/bug-6253-collection-trait.php', + ], + ); + $this->assertNoErrors($errors); + } + + public function testBug6442(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6442.php'); $this->assertCount(2, $errors); + $this->assertSame('Dumped type: \'Bug6442\\\A\'', $errors[0]->getMessage()); + $this->assertSame(9, $errors[0]->getLine()); + $this->assertSame('Dumped type: \'Bug6442\\\B\'', $errors[1]->getMessage()); + $this->assertSame(9, $errors[1]->getLine()); + } + + public function testBug6375(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6375.php'); + $this->assertNoErrors($errors); + } + + public function testBug6501(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6501.php'); + $this->assertNoErrors($errors); + } + + public function testBug6114(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } - $this->assertSame('Access to an undefined static property static(Bug4734\Foo)::$httpMethodParameterOverride3.', $errors[0]->getMessage()); - $this->assertSame('Access to an undefined property Bug4734\Foo::$httpMethodParameterOverride4.', $errors[1]->getMessage()); + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6114.php'); + $this->assertNoErrors($errors); + } + + public function testBug6681(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6681.php'); + $this->assertNoErrors($errors); + } + + public function testBug6212(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6212.php'); + $this->assertNoErrors($errors); + } + + public function testBug6740(): void + { + $errors = $this->runAnalyse(__DIR__ . '/data/bug-6740-b.php'); + $this->assertNoErrors($errors); } /** - * @param string $file - * @return \PHPStan\Analyser\Error[] + * @param string[]|null $allAnalysedFiles + * @return Error[] */ - private function runAnalyse(string $file): array + private function runAnalyse(string $file, ?array $allAnalysedFiles = null): array { $file = $this->getFileHelper()->normalizePath($file); - /** @var \PHPStan\Analyser\Analyser $analyser */ + /** @var Analyser $analyser */ $analyser = self::getContainer()->getByType(Analyser::class); - /** @var \PHPStan\File\FileHelper $fileHelper */ + /** @var FileHelper $fileHelper */ $fileHelper = self::getContainer()->getByType(FileHelper::class); - /** @var \PHPStan\Analyser\Error[] $errors */ - $errors = $analyser->analyse([$file])->getErrors(); + /** @var Error[] $errors */ + $errors = $analyser->analyse([$file], null, null, true, $allAnalysedFiles)->getErrors(); foreach ($errors as $error) { $this->assertSame($fileHelper->normalizePath($file), $error->getFilePath()); } diff --git a/tests/PHPStan/Analyser/AnalyserTest.php b/tests/PHPStan/Analyser/AnalyserTest.php index 2547c6985e..55be9d0d26 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -2,27 +2,35 @@ namespace PHPStan\Analyser; +use PhpParser\Lexer; +use PhpParser\NodeVisitor\NameResolver; use PhpParser\NodeVisitor\NodeConnectingVisitor; -use PHPStan\Broker\AnonymousClassNameHelper; -use PHPStan\Cache\Cache; -use PHPStan\Command\IgnoredRegexValidator; +use PhpParser\Parser\Php7; +use PhpParser\PrettyPrinter\Standard; use PHPStan\Dependency\DependencyResolver; use PHPStan\Dependency\ExportedNodeResolver; use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider; -use PHPStan\File\RelativePathHelper; use PHPStan\NodeVisitor\StatementOrderVisitor; use PHPStan\Parser\RichParser; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocInheritanceResolver; -use PHPStan\PhpDoc\PhpDocNodeResolver; -use PHPStan\PhpDoc\PhpDocStringResolver; - -use PHPStan\Reflection\ReflectionProvider\DirectReflectionProviderProvider; +use PHPStan\PhpDoc\StubPhpDocProvider; use PHPStan\Rules\AlwaysFailRule; use PHPStan\Rules\Registry; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\FileTypeMapper; - -class AnalyserTest extends \PHPStan\Testing\TestCase +use function array_map; +use function array_merge; +use function assert; +use function count; +use function is_string; +use function sprintf; +use function str_replace; +use function strtoupper; +use function substr; +use const PHP_OS; + +class AnalyserTest extends PHPStanTestCase { public function testReturnErrorIfIgnoredMessagesDoesNotOccur(): void @@ -45,14 +53,6 @@ public function testDoNotReturnErrorIfIgnoredMessagesDoNotOccurWhileAnalysingInd $this->assertEmpty($result); } - public function testReportInvalidIgnorePatternEarly(): void - { - $result = $this->runAnalyser(['#Regexp syntax error'], true, __DIR__ . '/data/parse-error.php', false); - $this->assertSame([ - "No ending delimiter '#' found in pattern: #Regexp syntax error", - ], $result); - } - public function testFileWithAnIgnoredError(): void { $result = $this->runAnalyser(['#Fail\.#'], true, __DIR__ . '/data/bootstrap-error.php', false); @@ -78,7 +78,7 @@ public function testIgnoreErrorByPath(): void ], ]; $result = $this->runAnalyser($ignoreErrors, true, __DIR__ . '/data/bootstrap-error.php', false); - $this->assertCount(0, $result); + $this->assertNoErrors($result); } public function testIgnoreErrorByPathAndCount(): void @@ -91,7 +91,7 @@ public function testIgnoreErrorByPathAndCount(): void ], ]; $result = $this->runAnalyser($ignoreErrors, true, __DIR__ . '/data/two-fails.php', false); - $this->assertCount(0, $result); + $this->assertNoErrors($result); } public function dataTrueAndFalse(): array @@ -104,7 +104,6 @@ public function dataTrueAndFalse(): array /** * @dataProvider dataTrueAndFalse - * @param bool $onlyFiles */ public function testIgnoreErrorByPathAndCountMoreThanExpected(bool $onlyFiles): void { @@ -136,7 +135,6 @@ public function testIgnoreErrorByPathAndCountMoreThanExpected(bool $onlyFiles): /** * @dataProvider dataTrueAndFalse - * @param bool $onlyFiles */ public function testIgnoreErrorByPathAndCountLessThanExpected(bool $onlyFiles): void { @@ -197,7 +195,7 @@ public function testIgnoreErrorByPaths(): void ], ]; $result = $this->runAnalyser($ignoreErrors, true, __DIR__ . '/data/bootstrap-error.php', false); - $this->assertCount(0, $result); + $this->assertNoErrors($result); } public function testIgnoreErrorByPathsMultipleUnmatched(): void @@ -257,7 +255,6 @@ public function dataIgnoreErrorInTraitUsingClassFilePath(): array /** * @dataProvider dataIgnoreErrorInTraitUsingClassFilePath - * @param string $pathToIgnore */ public function testIgnoreErrorInTraitUsingClassFilePath(string $pathToIgnore): void { @@ -271,7 +268,7 @@ public function testIgnoreErrorInTraitUsingClassFilePath(string $pathToIgnore): __DIR__ . '/data/traits-ignore/Foo.php', __DIR__ . '/data/traits-ignore/FooTrait.php', ], true); - $this->assertCount(0, $result); + $this->assertNoErrors($result); } public function testIgnoredErrorMissingMessage(): void @@ -305,19 +302,6 @@ public function testIgnoredErrorMissingPath(): void $this->assertSame('Ignored error {"message":"#Fail\\\\.#"} is missing a path.', $result[0]); } - public function testIgnoredErrorMessageStillValidatedIfMissingAPath(): void - { - $ignoreErrors = [ - [ - 'message' => '#Fail\.', - ], - ]; - $result = $this->runAnalyser($ignoreErrors, true, __DIR__ . '/data/empty/empty.php', false); - $this->assertCount(2, $result); - $this->assertSame('Ignored error {"message":"#Fail\\\\."} is missing a path.', $result[0]); - $this->assertSame('No ending delimiter \'#\' found in pattern: #Fail\.', $result[1]); - } - public function testReportMultipleParserErrorsAtOnce(): void { $result = $this->runAnalyser([], false, __DIR__ . '/data/multipleParseErrors.php', false); @@ -336,7 +320,6 @@ public function testReportMultipleParserErrorsAtOnce(): void /** * @dataProvider dataTrueAndFalse - * @param bool $onlyFiles */ public function testDoNotReportUnmatchedIgnoredErrorsFromPathIfPathWasNotAnalysed(bool $onlyFiles): void { @@ -353,12 +336,11 @@ public function testDoNotReportUnmatchedIgnoredErrorsFromPathIfPathWasNotAnalyse $result = $this->runAnalyser($ignoreErrors, true, [ __DIR__ . '/data/two-fails.php', ], $onlyFiles); - $this->assertCount(0, $result); + $this->assertNoErrors($result); } /** * @dataProvider dataTrueAndFalse - * @param bool $onlyFiles */ public function testDoNotReportUnmatchedIgnoredErrorsFromPathWithCountIfPathWasNotAnalysed(bool $onlyFiles): void { @@ -377,12 +359,11 @@ public function testDoNotReportUnmatchedIgnoredErrorsFromPathWithCountIfPathWasN $result = $this->runAnalyser($ignoreErrors, true, [ __DIR__ . '/data/two-fails.php', ], $onlyFiles); - $this->assertCount(0, $result); + $this->assertNoErrors($result); } /** * @dataProvider dataTrueAndFalse - * @param bool $reportUnmatchedIgnoredErrors */ public function testIgnoreNextLine(bool $reportUnmatchedIgnoredErrors): void { @@ -409,7 +390,6 @@ public function testIgnoreNextLine(bool $reportUnmatchedIgnoredErrors): void /** * @dataProvider dataTrueAndFalse - * @param bool $reportUnmatchedIgnoredErrors */ public function testIgnoreLine(bool $reportUnmatchedIgnoredErrors): void { @@ -436,16 +416,14 @@ public function testIgnoreLine(bool $reportUnmatchedIgnoredErrors): void /** * @param mixed[] $ignoreErrors - * @param bool $reportUnmatchedIgnoredErrors * @param string|string[] $filePaths - * @param bool $onlyFiles - * @return string[]|\PHPStan\Analyser\Error[] + * @return string[]|Error[] */ private function runAnalyser( array $ignoreErrors, bool $reportUnmatchedIgnoredErrors, $filePaths, - bool $onlyFiles + bool $onlyFiles, ): array { $analyser = $this->createAnalyser($reportUnmatchedIgnoredErrors); @@ -455,19 +433,16 @@ private function runAnalyser( } $ignoredErrorHelper = new IgnoredErrorHelper( - self::getContainer()->getByType(IgnoredRegexValidator::class), $this->getFileHelper(), $ignoreErrors, - $reportUnmatchedIgnoredErrors + $reportUnmatchedIgnoredErrors, ); $ignoredErrorHelperResult = $ignoredErrorHelper->initialize(); if (count($ignoredErrorHelperResult->getErrors()) > 0) { return $ignoredErrorHelperResult->getErrors(); } - $normalizedFilePaths = array_map(function (string $path): string { - return $this->getFileHelper()->normalizePath($path); - }, $filePaths); + $normalizedFilePaths = array_map(fn (string $path): string => $this->getFileHelper()->normalizePath($path), $filePaths); $analyserResult = $analyser->analyse($normalizedFilePaths); @@ -478,67 +453,62 @@ private function runAnalyser( return array_merge( $errors, - $ignoredErrorHelperResult->getWarnings(), - $analyserResult->getInternalErrors() + $analyserResult->getInternalErrors(), ); } - private function createAnalyser(bool $reportUnmatchedIgnoredErrors): \PHPStan\Analyser\Analyser + private function createAnalyser(bool $reportUnmatchedIgnoredErrors): Analyser { $registry = new Registry([ new AlwaysFailRule(), ]); - $broker = $this->createBroker(); - $printer = new \PhpParser\PrettyPrinter\Standard(); + $reflectionProvider = $this->createReflectionProvider(); + $printer = new Standard(); $fileHelper = $this->getFileHelper(); - /** @var RelativePathHelper $relativePathHelper */ - $relativePathHelper = self::getContainer()->getService('simpleRelativePathHelper'); - $phpDocStringResolver = self::getContainer()->getByType(PhpDocStringResolver::class); - $phpDocNodeResolver = self::getContainer()->getByType(PhpDocNodeResolver::class); - $typeSpecifier = $this->createTypeSpecifier($printer, $broker); - $fileTypeMapper = new FileTypeMapper(new DirectReflectionProviderProvider($broker), $this->getParser(), $phpDocStringResolver, $phpDocNodeResolver, $this->createMock(Cache::class), new AnonymousClassNameHelper($fileHelper, $relativePathHelper)); - $phpDocInheritanceResolver = new PhpDocInheritanceResolver($fileTypeMapper); + $typeSpecifier = self::getContainer()->getService('typeSpecifier'); + $fileTypeMapper = self::getContainer()->getByType(FileTypeMapper::class); + $phpDocInheritanceResolver = new PhpDocInheritanceResolver($fileTypeMapper, self::getContainer()->getByType(StubPhpDocProvider::class)); $nodeScopeResolver = new NodeScopeResolver( - $broker, - self::getReflectors()[0], + $reflectionProvider, + self::getReflector(), $this->getClassReflectionExtensionRegistryProvider(), $this->getParser(), $fileTypeMapper, + self::getContainer()->getByType(StubPhpDocProvider::class), self::getContainer()->getByType(PhpVersion::class), $phpDocInheritanceResolver, $fileHelper, $typeSpecifier, self::getContainer()->getByType(DynamicThrowTypeExtensionProvider::class), false, - false, true, [], [], true, - true ); - $lexer = new \PhpParser\Lexer(['usedAttributes' => ['comments', 'startLine', 'endLine', 'startTokenPos', 'endTokenPos']]); + $lexer = new Lexer(['usedAttributes' => ['comments', 'startLine', 'endLine', 'startTokenPos', 'endTokenPos']]); $fileAnalyser = new FileAnalyser( - $this->createScopeFactory($broker, $typeSpecifier), + $this->createScopeFactory($reflectionProvider, $typeSpecifier), $nodeScopeResolver, new RichParser( - new \PhpParser\Parser\Php7($lexer), - new \PhpParser\NodeVisitor\NameResolver(), + new Php7($lexer), + $lexer, + new NameResolver(), new NodeConnectingVisitor(), - new StatementOrderVisitor() + new StatementOrderVisitor(), ), - new DependencyResolver($fileHelper, $broker, new ExportedNodeResolver($fileTypeMapper, $printer)), - $reportUnmatchedIgnoredErrors + new DependencyResolver($fileHelper, $reflectionProvider, new ExportedNodeResolver($fileTypeMapper, $printer)), + $reportUnmatchedIgnoredErrors, ); return new Analyser( $fileAnalyser, $registry, $nodeScopeResolver, - 50 + 50, ); } diff --git a/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php index dad3508986..611d11121d 100644 --- a/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php @@ -3,12 +3,14 @@ namespace PHPStan\Analyser; use PHPStan\File\FileHelper; +use PHPStan\Testing\PHPStanTestCase; +use function array_map; +use function sprintf; -class AnalyserTraitsIntegrationTest extends \PHPStan\Testing\TestCase +class AnalyserTraitsIntegrationTest extends PHPStanTestCase { - /** @var \PHPStan\File\FileHelper */ - private $fileHelper; + private FileHelper $fileHelper; protected function setUp(): void { @@ -35,7 +37,7 @@ public function testMethodDoesNotExist(): void $this->assertSame('Call to an undefined method AnalyseTraits\Bar::doFoo().', $error->getMessage()); $this->assertSame( sprintf('%s (in context of class AnalyseTraits\Bar)', $this->fileHelper->normalizePath(__DIR__ . '/traits/FooTrait.php')), - $error->getFile() + $error->getFile(), ); $this->assertSame(10, $error->getLine()); } @@ -52,7 +54,7 @@ public function testNestedTraits(): void $this->assertSame('Call to an undefined method AnalyseTraits\NestedBar::doFoo().', $firstError->getMessage()); $this->assertSame( sprintf('%s (in context of class AnalyseTraits\NestedBar)', $this->fileHelper->normalizePath(__DIR__ . '/traits/FooTrait.php')), - $firstError->getFile() + $firstError->getFile(), ); $this->assertSame(10, $firstError->getLine()); @@ -60,7 +62,7 @@ public function testNestedTraits(): void $this->assertSame('Call to an undefined method AnalyseTraits\NestedBar::doNestedFoo().', $secondError->getMessage()); $this->assertSame( sprintf('%s (in context of class AnalyseTraits\NestedBar)', $this->fileHelper->normalizePath(__DIR__ . '/traits/NestedFooTrait.php')), - $secondError->getFile() + $secondError->getFile(), ); $this->assertSame(12, $secondError->getLine()); } @@ -100,7 +102,7 @@ public function testTraitInAnonymousClass(): void [ __DIR__ . '/traits/AnonymousClassUsingTrait.php', __DIR__ . '/traits/TraitWithTypeSpecification.php', - ] + ], ); $this->assertCount(1, $errors); $this->assertStringContainsString('Access to an undefined property', $errors[0]->getMessage()); @@ -110,7 +112,7 @@ public function testTraitInAnonymousClass(): void public function testDuplicateMethodDefinition(): void { $errors = $this->runAnalyse([__DIR__ . '/traits/duplicateMethod/Lesson.php']); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testWrongPropertyType(): void @@ -120,14 +122,14 @@ public function testWrongPropertyType(): void $this->assertSame(15, $errors[0]->getLine()); $this->assertSame( $this->fileHelper->normalizePath(__DIR__ . '/traits/wrongProperty/Foo.php'), - $errors[0]->getFile() + $errors[0]->getFile(), ); $this->assertSame('Property TraitsWrongProperty\Foo::$id (int) does not accept string.', $errors[0]->getMessage()); $this->assertSame(17, $errors[1]->getLine()); $this->assertSame( $this->fileHelper->normalizePath(__DIR__ . '/traits/wrongProperty/Foo.php'), - $errors[1]->getFile() + $errors[1]->getFile(), ); $this->assertSame('Property TraitsWrongProperty\Foo::$bar (Ipsum) does not accept int.', $errors[1]->getMessage()); } @@ -145,13 +147,13 @@ public function testReturnThis(): void public function testTraitInEval(): void { $errors = $this->runAnalyse([__DIR__ . '/traits/TraitInEvalUse.php']); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testParameterNotFoundCrash(): void { $errors = $this->runAnalyse([__DIR__ . '/traits/parameter-not-found.php']); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } public function testMissingReturnInAbstractTraitMethod(): void @@ -160,21 +162,19 @@ public function testMissingReturnInAbstractTraitMethod(): void __DIR__ . '/traits/TraitWithAbstractMethod.php', __DIR__ . '/traits/ClassImplementingTraitWithAbstractMethod.php', ]); - $this->assertCount(0, $errors); + $this->assertNoErrors($errors); } /** * @param string[] $files - * @return \PHPStan\Analyser\Error[] + * @return Error[] */ private function runAnalyse(array $files): array { - $files = array_map(function (string $file): string { - return $this->getFileHelper()->normalizePath($file); - }, $files); - /** @var \PHPStan\Analyser\Analyser $analyser */ + $files = array_map(fn (string $file): string => $this->getFileHelper()->normalizePath($file), $files); + /** @var Analyser $analyser */ $analyser = self::getContainer()->getByType(Analyser::class); - /** @var \PHPStan\Analyser\Error[] $errors */ + /** @var Error[] $errors */ $errors = $analyser->analyse($files)->getErrors(); return $errors; } diff --git a/tests/PHPStan/Analyser/AnonymousClassNameRule.php b/tests/PHPStan/Analyser/AnonymousClassNameRule.php index a4cf2bddc6..0bba0df265 100644 --- a/tests/PHPStan/Analyser/AnonymousClassNameRule.php +++ b/tests/PHPStan/Analyser/AnonymousClassNameRule.php @@ -4,18 +4,15 @@ use PhpParser\Node; use PhpParser\Node\Stmt\Class_; +use PHPStan\Broker\ClassNotFoundException; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; class AnonymousClassNameRule implements Rule { - /** @var ReflectionProvider */ - private $reflectionProvider; - - public function __construct(ReflectionProvider $reflectionProvider) + public function __construct(private ReflectionProvider $reflectionProvider) { - $this->reflectionProvider = $reflectionProvider; } public function getNodeType(): string @@ -25,7 +22,6 @@ public function getNodeType(): string /** * @param Class_ $node - * @param Scope $scope * @return string[] */ public function processNode(Node $node, Scope $scope): array @@ -35,7 +31,7 @@ public function processNode(Node $node, Scope $scope): array : (string) $node->name; try { $this->reflectionProvider->getClass($className); - } catch (\PHPStan\Broker\ClassNotFoundException $e) { + } catch (ClassNotFoundException) { return ['not found']; } diff --git a/tests/PHPStan/Analyser/AnonymousClassNameRuleTest.php b/tests/PHPStan/Analyser/AnonymousClassNameRuleTest.php index 2ea1a331d7..c5f5a5aab1 100644 --- a/tests/PHPStan/Analyser/AnonymousClassNameRuleTest.php +++ b/tests/PHPStan/Analyser/AnonymousClassNameRuleTest.php @@ -10,8 +10,8 @@ class AnonymousClassNameRuleTest extends RuleTestCase protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new AnonymousClassNameRule($broker); + $reflectionProvider = $this->createReflectionProvider(); + return new AnonymousClassNameRule($reflectionProvider); } public function testRule(): void diff --git a/tests/PHPStan/Analyser/ClassConstantStubFileTest.php b/tests/PHPStan/Analyser/ClassConstantStubFileTest.php new file mode 100644 index 0000000000..00d83693e6 --- /dev/null +++ b/tests/PHPStan/Analyser/ClassConstantStubFileTest.php @@ -0,0 +1,35 @@ +gatherAssertTypes(__DIR__ . '/data/class-constant-stub-files.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__ . '/classConstantStubFiles.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/DynamicMethodThrowTypeExtensionTest.php b/tests/PHPStan/Analyser/DynamicMethodThrowTypeExtensionTest.php index bdb8094fe9..39b17b66f7 100644 --- a/tests/PHPStan/Analyser/DynamicMethodThrowTypeExtensionTest.php +++ b/tests/PHPStan/Analyser/DynamicMethodThrowTypeExtensionTest.php @@ -14,14 +14,12 @@ public function dataFileAsserts(): iterable /** * @dataProvider dataFileAsserts - * @param string $assertType - * @param string $file * @param mixed ...$args */ public function testFileAsserts( string $assertType, string $file, - ...$args + ...$args, ): void { $this->assertFileAsserts($assertType, $file, ...$args); diff --git a/tests/PHPStan/Analyser/DynamicReturnTypeExtensionTypeInferenceTest.php b/tests/PHPStan/Analyser/DynamicReturnTypeExtensionTypeInferenceTest.php new file mode 100644 index 0000000000..85eebb189f --- /dev/null +++ b/tests/PHPStan/Analyser/DynamicReturnTypeExtensionTypeInferenceTest.php @@ -0,0 +1,36 @@ +gatherAssertTypes(__DIR__ . '/data/dynamic-method-return-types.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/dynamic-method-return-compound-types.php'); + } + + /** + * @dataProvider dataAsserts + * @param mixed ...$args + */ + public function testAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/dynamic-return-type.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/ErrorTest.php b/tests/PHPStan/Analyser/ErrorTest.php index d2347ebbfc..5e7d1e8ad0 100644 --- a/tests/PHPStan/Analyser/ErrorTest.php +++ b/tests/PHPStan/Analyser/ErrorTest.php @@ -2,7 +2,9 @@ namespace PHPStan\Analyser; -class ErrorTest extends \PHPStan\Testing\TestCase +use PHPStan\Testing\PHPStanTestCase; + +class ErrorTest extends PHPStanTestCase { public function testError(): void diff --git a/tests/PHPStan/Analyser/EvaluationOrderRule.php b/tests/PHPStan/Analyser/EvaluationOrderRule.php index de08902369..14fe278783 100644 --- a/tests/PHPStan/Analyser/EvaluationOrderRule.php +++ b/tests/PHPStan/Analyser/EvaluationOrderRule.php @@ -14,8 +14,6 @@ public function getNodeType(): string } /** - * @param Node $node - * @param Scope $scope * @return string[] */ public function processNode(Node $node, Scope $scope): array diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 9db0b9dc09..ac031bd37e 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -3,38 +3,40 @@ namespace PHPStan\Analyser; use Generator; +use PhpParser\Node; use PhpParser\Node\Expr\Exit_; -use PhpParser\Node\Expr\MethodCall; -use PhpParser\Node\Expr\StaticCall; +use PhpParser\PrettyPrinter\Standard; use PHPStan\Node\VirtualNode; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\ShouldNotHappenException; use PHPStan\Testing\TypeInferenceTestCase; -use PHPStan\Tests\AssertionClassMethodTypeSpecifyingExtension; -use PHPStan\Tests\AssertionClassStaticMethodTypeSpecifyingExtension; use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\DynamicMethodReturnTypeExtension; -use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; -use PHPStan\Type\ObjectType; -use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use SomeNodeScopeResolverNamespace\Foo; +use function define; +use function defined; +use function function_exists; +use function is_bool; +use function is_float; +use function is_int; +use function is_string; +use function sprintf; +use function str_replace; use const PHP_VERSION_ID; class LegacyNodeScopeResolverTest extends TypeInferenceTestCase { /** @var Scope[][] */ - private static $assertTypesCache = []; + private static array $assertTypesCache = []; public function testClassMethodScope(): void { - $this->processFile(__DIR__ . '/data/class.php', function (\PhpParser\Node $node, Scope $scope): void { + $this->processFile(__DIR__ . '/data/class.php', function (Node $node, Scope $scope): void { if (!($node instanceof Exit_)) { return; } @@ -63,9 +65,9 @@ public function testClassMethodScope(): void private function getFileScope(string $filename): Scope { - /** @var \PHPStan\Analyser\Scope $testScope */ + /** @var Scope $testScope */ $testScope = null; - $this->processFile($filename, static function (\PhpParser\Node $node, Scope $scope) use (&$testScope): void { + $this->processFile($filename, static function (Node $node, Scope $scope) use (&$testScope): void { if (!($node instanceof Exit_)) { return; } @@ -88,18 +90,16 @@ public function dataUnionInCatch(): array /** * @dataProvider dataUnionInCatch - * @param string $description - * @param string $expression */ public function testUnionInCatch( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/catch-union.php', $description, - $expression + $expression, ); } @@ -175,7 +175,7 @@ public function dataUnionAndIntersection(): array 'self::IPSUM_CONSTANT', ], [ - 'array(1, 2, 3)', + 'array{1, 2, 3}', 'parent::PARENT_CONSTANT', ], [ @@ -215,18 +215,16 @@ public function dataUnionAndIntersection(): array /** * @dataProvider dataUnionAndIntersection - * @param string $description - * @param string $expression */ public function testUnionAndIntersection( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/union-intersection.php', $description, - $expression + $expression, ); } @@ -262,19 +260,19 @@ public function dataAssignInIf(): array $testScope, 'arrOne', TrinaryLogic::createYes(), - 'array(\'one\')', + 'array{\'one\'}', ], [ $testScope, 'arrTwo', TrinaryLogic::createYes(), - 'array(\'test\' => \'two\', 0 => Foo)', + 'array{test: \'two\', 0: Foo}', ], [ $testScope, 'arrThree', TrinaryLogic::createYes(), - 'array(\'three\')', + 'array{\'three\'}', ], [ $testScope, @@ -286,19 +284,19 @@ public function dataAssignInIf(): array $testScope, 'i', TrinaryLogic::createYes(), - 'int', + 'int<0, 4>', ], [ $testScope, 'f', TrinaryLogic::createMaybe(), - 'int', + 'int<1, max>', ], [ $testScope, 'anotherF', TrinaryLogic::createYes(), - 'int', + 'int<1, max>', ], [ $testScope, @@ -310,7 +308,7 @@ public function dataAssignInIf(): array $testScope, 'anotherArray', TrinaryLogic::createYes(), - 'array(\'test\' => array(\'another\'))', + 'array{test: array{\'another\'}}', ], [ $testScope, @@ -423,7 +421,7 @@ public function dataAssignInIf(): array $testScope, 'previousI', TrinaryLogic::createYes(), - '0|1', + 'int<1, max>', ], [ $testScope, @@ -435,7 +433,7 @@ public function dataAssignInIf(): array $testScope, 'frame', TrinaryLogic::createYes(), - 'mixed', + 'mixed~null', ], [ $testScope, @@ -495,13 +493,13 @@ public function dataAssignInIf(): array $testScope, 'nullableIntegers', TrinaryLogic::createYes(), - 'array(1, 2, 3, null)', + 'array{1, 2, 3, null}', ], [ $testScope, 'union', TrinaryLogic::createYes(), - 'array(1, 2, 3, \'foo\')', + 'array{1, 2, 3, \'foo\'}', '1|2|3|\'foo\'', ], [ @@ -584,14 +582,14 @@ public function dataAssignInIf(): array [ $testScope, 'nonexistentVariableOutsideFor', - TrinaryLogic::createMaybe(), + TrinaryLogic::createYes(), '1', ], [ $testScope, 'integerOrNullFromFor', TrinaryLogic::createYes(), - '1|null', + '1', ], [ $testScope, @@ -669,7 +667,7 @@ public function dataAssignInIf(): array $testScope, 'arrayOfIntegers', TrinaryLogic::createYes(), - 'array(1, 2, 3)', + 'array{1, 2, 3}', ], [ $testScope, @@ -699,7 +697,7 @@ public function dataAssignInIf(): array $testScope, 'mixed', TrinaryLogic::createYes(), - 'mixed', // should be mixed~bool+1 + 'mixed~bool', ], [ $testScope, @@ -742,18 +740,13 @@ public function dataAssignInIf(): array /** * @dataProvider dataAssignInIf - * @param \PHPStan\Analyser\Scope $scope - * @param string $variableName - * @param \PHPStan\TrinaryLogic $expectedCertainty - * @param string|null $typeDescription - * @param string|null $iterableValueTypeDescription */ public function testAssignInIf( Scope $scope, string $variableName, TrinaryLogic $expectedCertainty, ?string $typeDescription = null, - ?string $iterableValueTypeDescription = null + ?string $iterableValueTypeDescription = null, ): void { $this->assertVariables( @@ -761,7 +754,7 @@ public function testAssignInIf( $variableName, $expectedCertainty, $typeDescription, - $iterableValueTypeDescription + $iterableValueTypeDescription, ); } @@ -793,7 +786,7 @@ public function dataConstantTypes(): array [ $testScope, 'literalArray', - 'array(\'a\' => 2, \'b\' => 4, \'c\' => 2, \'d\' => 4)', + 'array{a: 2, b: 4, c: 2, d: 4}', ], [ $testScope, @@ -823,17 +816,17 @@ public function dataConstantTypes(): array [ $testScope, 'incrementInForLoop', - 'int', + 'int<2, max>', ], [ $testScope, 'valueOverwrittenInForLoop', - '1|2', + '2', ], [ $testScope, 'arrayOverwrittenInForLoop', - 'array(\'a\' => int, \'b\' => \'bar\'|\'foo\')', + 'array{a: int<2, max>, b: \'bar\'}', ], [ $testScope, @@ -843,12 +836,12 @@ public function dataConstantTypes(): array [ $testScope, 'intProperty', - 'int', + 'int<2, max>', ], [ $testScope, 'staticIntProperty', - 'int', + 'int<2, max>', ], [ $testScope, @@ -863,7 +856,7 @@ public function dataConstantTypes(): array [ $testScope, 'variableIncrementedInClosurePassedByReference', - 'int', + 'int<0, max>', ], [ $testScope, @@ -873,7 +866,7 @@ public function dataConstantTypes(): array [ $testScope, 'yetAnotherVariableInClosurePassedByReference', - 'int', + '0|1', ], [ $testScope, @@ -885,14 +878,11 @@ public function dataConstantTypes(): array /** * @dataProvider dataConstantTypes - * @param \PHPStan\Analyser\Scope $scope - * @param string $variableName - * @param string $typeDescription */ public function testConstantTypes( Scope $scope, string $variableName, - string $typeDescription + string $typeDescription, ): void { $this->assertVariables( @@ -900,7 +890,7 @@ public function testConstantTypes( $variableName, TrinaryLogic::createYes(), $typeDescription, - null + null, ); } @@ -909,7 +899,7 @@ private function assertVariables( string $variableName, TrinaryLogic $expectedCertainty, ?string $typeDescription = null, - ?string $iterableValueTypeDescription = null + ?string $iterableValueTypeDescription = null, ): void { $certainty = $scope->hasVariableType($variableName); @@ -919,8 +909,8 @@ private function assertVariables( 'Certainty of variable $%s is %s, expected %s', $variableName, $certainty->describe(), - $expectedCertainty->describe() - ) + $expectedCertainty->describe(), + ), ); if (!$expectedCertainty->no()) { if ($typeDescription === null) { @@ -930,14 +920,14 @@ private function assertVariables( $this->assertSame( $typeDescription, $scope->getVariableType($variableName)->describe(VerbosityLevel::precise()), - sprintf('Type of variable $%s does not match the expected one.', $variableName) + sprintf('Type of variable $%s does not match the expected one.', $variableName), ); if ($iterableValueTypeDescription !== null) { $this->assertSame( $iterableValueTypeDescription, $scope->getVariableType($variableName)->getIterableValueType()->describe(VerbosityLevel::precise()), - sprintf('Iterable value type of variable $%s does not match the expected one.', $variableName) + sprintf('Iterable value type of variable $%s does not match the expected one.', $variableName), ); } } elseif ($typeDescription !== null) { @@ -945,8 +935,8 @@ private function assertVariables( sprintf( 'No type should be asserted for an undefined variable $%s, %s given.', $variableName, - $typeDescription - ) + $typeDescription, + ), ); } } @@ -1223,18 +1213,16 @@ public function dataArrayDestructuring(): array /** * @dataProvider dataArrayDestructuring - * @param string $description - * @param string $expression */ public function testArrayDestructuring( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/array-destructuring.php', $description, - $expression + $expression, ); } @@ -1294,7 +1282,7 @@ public function dataParameterTypes(): array '$callable', ], [ - 'array', + PHP_VERSION_ID < 80000 ? 'array' : 'array', '$variadicStrings', ], [ @@ -1306,18 +1294,16 @@ public function dataParameterTypes(): array /** * @dataProvider dataParameterTypes - * @param string $typeClass - * @param string $expression */ public function testTypehints( string $typeClass, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/typehints.php', $typeClass, - $expression + $expression, ); } @@ -1369,18 +1355,16 @@ public function dataAnonymousFunctionParameterTypes(): array /** * @dataProvider dataAnonymousFunctionParameterTypes - * @param string $description - * @param string $expression */ public function testAnonymousFunctionTypehints( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/typehints-anonymous-function.php', $description, - $expression + $expression, ); } @@ -1452,25 +1436,19 @@ public function dataVarAnnotations(): array /** * @dataProvider dataVarAnnotations - * @param string $description - * @param string $expression */ public function testVarAnnotations( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/var-annotations.php', $description, $expression, - [], - [], - [], - [], 'die', [], - false + false, ); } @@ -1550,11 +1528,11 @@ public function dataCasts(): array '(float) "5"', ], [ - '*ERROR*', + '0', '(int) "blabla"', ], [ - '*ERROR*', + '0.0', '(float) "blabla"', ], [ @@ -1574,31 +1552,31 @@ public function dataCasts(): array '(float) $str', ], [ - 'array(\'\' . "\0" . \'TypesNamespaceCasts\\\\Foo\' . "\0" . \'foo\' => TypesNamespaceCasts\Foo, \'\' . "\0" . \'TypesNamespaceCasts\\\\Foo\' . "\0" . \'int\' => int, \'\' . "\0" . \'*\' . "\0" . \'protectedInt\' => int, \'publicInt\' => int, \'\' . "\0" . \'TypesNamespaceCasts\\\\Bar\' . "\0" . \'barProperty\' => TypesNamespaceCasts\Bar)', + "array{\0TypesNamespaceCasts\\Foo\0foo: TypesNamespaceCasts\\Foo, \0TypesNamespaceCasts\\Foo\0int: int, \0*\0protectedInt: int, publicInt: int, \0TypesNamespaceCasts\\Bar\0barProperty: TypesNamespaceCasts\\Bar}", '(array) $foo', ], [ - 'array(1, 2, 3)', + 'array{1, 2, 3}', '(array) [1, 2, 3]', ], [ - 'array(1)', + 'array{1}', '(array) 1', ], [ - 'array(1.0)', + 'array{1.0}', '(array) 1.0', ], [ - 'array(true)', + 'array{true}', '(array) true', ], [ - 'array(\'blabla\')', + 'array{\'blabla\'}', '(array) "blabla"', ], [ - 'array(int)', + 'array{int}', '(array) $castedInteger', ], [ @@ -1614,18 +1592,16 @@ public function dataCasts(): array /** * @dataProvider dataCasts - * @param string $desciptiion - * @param string $expression */ public function testCasts( string $desciptiion, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/casts.php', $desciptiion, - $expression + $expression, ); } @@ -1641,23 +1617,21 @@ public function dataUnsetCast(): array /** * @dataProvider dataUnsetCast - * @param string $desciptiion - * @param string $expression */ public function testUnsetCast( string $desciptiion, - string $expression + string $expression, ): void { if (!self::$useStaticReflectionProvider && PHP_VERSION_ID >= 70200) { $this->markTestSkipped( - 'Test cannot be run on PHP 7.2 and higher - (unset) cast is deprecated.' + 'Test cannot be run on PHP 7.2 and higher - (unset) cast is deprecated.', ); } $this->assertTypes( __DIR__ . '/data/cast-unset.php', $desciptiion, - $expression + $expression, ); } @@ -1697,7 +1671,7 @@ public function dataDeductedTypes(): array '$loremObjectLiteral', ], [ - 'mixed~string', + 'object', '$mixedObjectLiteral', ], [ @@ -1705,7 +1679,7 @@ public function dataDeductedTypes(): array '$newStatic', ], [ - 'array()', + 'array{}', '$arrayLiteral', ], [ @@ -1737,7 +1711,7 @@ public function dataDeductedTypes(): array 'self::STRING_CONSTANT', ], [ - 'array()', + 'array{}', 'self::ARRAY_CONSTANT', ], [ @@ -1761,7 +1735,7 @@ public function dataDeductedTypes(): array '$foo::STRING_CONSTANT', ], [ - 'array()', + 'array{}', '$foo::ARRAY_CONSTANT', ], [ @@ -1777,19 +1751,17 @@ public function dataDeductedTypes(): array /** * @dataProvider dataDeductedTypes - * @param string $description - * @param string $expression */ public function testDeductedTypes( string $description, - string $expression + string $expression, ): void { require_once __DIR__ . '/data/function-definitions.php'; $this->assertTypes( __DIR__ . '/data/deducted-types.php', $description, - $expression + $expression, ); } @@ -1925,18 +1897,16 @@ public function dataProperties(): array /** * @dataProvider dataProperties - * @param string $description - * @param string $expression */ public function testProperties( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/properties.php', $description, - $expression + $expression, ); } @@ -1953,7 +1923,7 @@ public function dataBinaryOperations(): array return (new ConstantStringType($value))->describe(VerbosityLevel::precise()); } - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); }; return [ @@ -2096,7 +2066,7 @@ public function dataBinaryOperations(): array '1.2 ** 1.4', ], [ - $typeCallback(3.2 % 2.4), + '1', '3.2 % 2.4', ], [ @@ -2129,7 +2099,7 @@ public function dataBinaryOperations(): array '1 ** 1.4', ], [ - $typeCallback(3 % 2.4), + '1', '3 % 2.4', ], [ @@ -2178,7 +2148,7 @@ public function dataBinaryOperations(): array '$integer ** $integer', ], [ - $typeCallback(3.2 % 2), + '1', '3.2 % 2', ], [ @@ -2316,15 +2286,15 @@ public function dataBinaryOperations(): array 'false ? 1 : 2', ], [ - '12|string', + '12|non-empty-string', '$string ?: 12', ], [ - '12|string', + '12|non-empty-string', '$stringOrNull ?: 12', ], [ - '12|string', + '12|non-empty-string', '@$stringOrNull ?: 12', ], [ @@ -2396,7 +2366,7 @@ public function dataBinaryOperations(): array 'min([1, 2, 3])', ], [ - 'array(1, 2, 3)', + 'array{1, 2, 3}', 'min([1, 2, 3], [4, 5, 5])', ], [ @@ -2412,11 +2382,11 @@ public function dataBinaryOperations(): array 'min(0, ...[1, 2, 3])', ], [ - 'array(5, 6, 9)', + 'array{5, 6, 9}', 'max([1, 10, 8], [5, 6, 9])', ], [ - 'array(1, 1, 1, 1)', + 'array{1, 1, 1, 1}', 'max(array(2, 2, 2), array(1, 1, 1, 1))', ], [ @@ -2496,11 +2466,11 @@ public function dataBinaryOperations(): array 'min(1, 2.2, 3.3)', ], [ - 'string', + 'non-empty-string', '"Hello $world"', ], [ - 'string', + 'non-empty-string', '$string .= "str"', ], [ @@ -2636,31 +2606,31 @@ public function dataBinaryOperations(): array '!isset($foo)', ], [ - 'bool', + 'false', 'empty($foo)', ], [ - 'bool', + 'true', '!empty($foo)', ], [ - 'array(int, int, int)', + 'array{int, int, int}', '$arrayOfIntegers + $arrayOfIntegers', ], [ - 'array(int, int, int)', + 'array{int, int, int}', '$arrayOfIntegers += $arrayOfIntegers', ], [ - 'array(0 => 1, 1 => 1, 2 => 1, 3 => 1|2, 4 => 1|3, ?5 => 2|3, ?6 => 3)', + 'array{0: 1, 1: 1, 2: 1, 3: 1|2, 4: 1|3, 5?: 2|3, 6?: 3}', '$conditionalArray + $unshiftedConditionalArray', ], [ - 'array(0 => \'lorem\', 1 => stdClass, 2 => 1, 3 => 1, 4 => 1, ?5 => 2|3, ?6 => 3)', + 'array{0: \'lorem\', 1: stdClass, 2: 1, 3: 1, 4: 1, 5?: 2|3, 6?: 3}', '$unshiftedConditionalArray + $conditionalArray', ], [ - 'array(int, int, int)', + 'array{int, int, int}', '$arrayOfIntegers += ["foo"]', ], [ @@ -2672,7 +2642,7 @@ public function dataBinaryOperations(): array '@count($arrayOfIntegers)', ], [ - 'array(int, int, int)', + 'array{int, int, int}', '$anotherArray = $arrayOfIntegers', ], [ @@ -2772,15 +2742,15 @@ public function dataBinaryOperations(): array '$preIncArray[3]', ], [ - 'array(1 => 1, 2 => 2)', + 'array{1: 1, 2: 2}', '$preIncArray', ], [ - 'array(0 => 1, 2 => 3)', + 'array{0: 1, 2: 3}', '$postIncArray', ], [ - 'array(0 => array(1 => array(2 => 3)), 4 => array(5 => array(6 => 7)))', + 'array{0: array{1: array{2: 3}}, 4: array{5: array{6: 7}}}', '$anotherPostIncArray', ], [ @@ -2840,11 +2810,11 @@ public function dataBinaryOperations(): array '1 + "blabla"', ], [ - 'array(1, 2, 3)', + 'array{1, 2, 3}', '[1, 2, 3] + [4, 5, 6]', ], [ - 'array', + 'non-empty-array', '$arrayOfUnknownIntegers + [1, 2, 3]', ], [ @@ -2996,27 +2966,27 @@ public function dataBinaryOperations(): array '"$std bar"', ], [ - 'array<\'foo\'|int|stdClass>&nonEmpty', + 'non-empty-array<\'foo\'|int|stdClass>', '$arrToPush', ], [ - 'array<\'foo\'|int|stdClass>&nonEmpty', + 'non-empty-array<\'foo\'|int|stdClass>', '$arrToPush2', ], [ - 'array(0 => \'lorem\', 1 => 5, \'foo\' => stdClass, 2 => \'test\')', + 'array{0: \'lorem\', 1: 5, foo: stdClass, 2: \'test\'}', '$arrToUnshift', ], [ - 'array<\'lorem\'|int|stdClass>&nonEmpty', + 'non-empty-array<\'lorem\'|int|stdClass>', '$arrToUnshift2', ], [ - 'array(0 => \'lorem\', 1 => stdClass, 2 => 1, 3 => 1, 4 => 1, ?5 => 2|3, ?6 => 3)', + 'array{0: \'lorem\', 1: stdClass, 2: 1, 3: 1, 4: 1, 5?: 2|3, 6?: 3}', '$unshiftedConditionalArray', ], [ - 'array(\'dirname\' => string, \'basename\' => string, \'filename\' => string, ?\'extension\' => string)', + 'array{dirname: string, basename: string, filename: string, extension?: string}', 'pathinfo($string)', ], [ @@ -3072,15 +3042,15 @@ public function dataBinaryOperations(): array '$decrementedFooString', ], [ - 'string', + 'literal-string&non-empty-string', '$conditionalString . $conditionalString', ], [ - 'string', + 'literal-string&non-empty-string', '$conditionalString . $anotherConditionalString', ], [ - 'string', + 'literal-string&non-empty-string', '$anotherConditionalString . $conditionalString', ], [ @@ -3120,11 +3090,11 @@ public function dataBinaryOperations(): array 'in_array(\'baz\', [\'foo\', \'bar\'], true)', ], [ - 'array(2, 3)', + 'array{2, 3}', '$arrToShift', ], [ - 'array(1, 2)', + 'array{1, 2}', '$arrToPop', ], [ @@ -3172,7 +3142,7 @@ public function dataBinaryOperations(): array "sprintf('%s %s', 'foo', 'bar')", ], [ - 'array()|array(0 => \'password\'|\'username\', ?1 => \'password\')', + 'array{}|array{0: \'password\'|\'username\', 1?: \'password\'}', '$coalesceArray', ], [ @@ -3188,7 +3158,7 @@ public function dataBinaryOperations(): array '$shiftedNonEmptyArray', ], [ - 'array&nonEmpty', + 'non-empty-array', '$unshiftedArray', ], [ @@ -3196,7 +3166,7 @@ public function dataBinaryOperations(): array '$poppedNonEmptyArray', ], [ - 'array&nonEmpty', + 'non-empty-array', '$pushedArray', ], [ @@ -3204,7 +3174,7 @@ public function dataBinaryOperations(): array '$simpleXMLReturningXML', ], [ - 'string', + 'non-empty-string', '$xmlString', ], [ @@ -3232,18 +3202,16 @@ public function dataBinaryOperations(): array /** * @dataProvider dataBinaryOperations - * @param string $description - * @param string $expression */ public function testBinaryOperations( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/binary.php', $description, - $expression + $expression, ); } @@ -3259,18 +3227,16 @@ public function dataVarStatementAnnotation(): array /** * @dataProvider dataVarStatementAnnotation - * @param string $description - * @param string $expression */ public function testVarStatementAnnotation( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/var-stmt-annotation.php', $description, - $expression + $expression, ); } @@ -3286,18 +3252,16 @@ public function dataCloneOperators(): array /** * @dataProvider dataCloneOperators - * @param string $description - * @param string $expression */ public function testCloneOperators( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/clone.php', $description, - $expression + $expression, ); } @@ -3329,7 +3293,7 @@ public function dataLiteralArrays(): array '$integers[0] >= $integers[1] - 1', ], [ - 'array(\'foo\' => array(\'foo\' => array(\'foo\' => \'bar\')), \'bar\' => array(), \'baz\' => array(\'lorem\' => array()))', + 'array{foo: array{foo: array{foo: \'bar\'}}, bar: array{}, baz: array{lorem: array{}}}', '$nestedArray', ], [ @@ -3341,18 +3305,16 @@ public function dataLiteralArrays(): array /** * @dataProvider dataLiteralArrays - * @param string $description - * @param string $expression */ public function testLiteralArrays( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/literal-arrays.php', $description, - $expression + $expression, ); } @@ -3420,23 +3382,17 @@ public function dataLiteralArraysKeys(): array /** * @dataProvider dataLiteralArraysKeys - * @param string $description - * @param string $evaluatedPointExpressionType */ public function testLiteralArraysKeys( string $description, - string $evaluatedPointExpressionType + string $evaluatedPointExpressionType, ): void { $this->assertTypes( __DIR__ . '/data/literal-arrays-keys.php', $description, '$key', - [], - [], - [], - [], - $evaluatedPointExpressionType + $evaluatedPointExpressionType, ); } @@ -3468,18 +3424,16 @@ public function dataStringArrayAccess(): array /** * @dataProvider dataStringArrayAccess - * @param string $description - * @param string $expression */ public function testStringArrayAccess( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/string-array-access.php', $description, - $expression + $expression, ); } @@ -3646,19 +3600,17 @@ public function dataTypeFromFunctionFunctionPhpDocs(): array /** * @dataProvider dataTypeFromFunctionPhpDocs * @dataProvider dataTypeFromFunctionFunctionPhpDocs - * @param string $description - * @param string $expression */ public function testTypeFromFunctionPhpDocs( string $description, - string $expression + string $expression, ): void { require_once __DIR__ . '/data/functionPhpDocs.php'; $this->assertTypes( __DIR__ . '/data/functionPhpDocs.php', $description, - $expression + $expression, ); } @@ -3675,38 +3627,34 @@ public function dataTypeFromFunctionPrefixedPhpDocs(): array /** * @dataProvider dataTypeFromFunctionPhpDocs * @dataProvider dataTypeFromFunctionPrefixedPhpDocs - * @param string $description - * @param string $expression */ public function testTypeFromFunctionPhpDocsPsalmPrefix( string $description, - string $expression + string $expression, ): void { require_once __DIR__ . '/data/functionPhpDocs-psalmPrefix.php'; $this->assertTypes( __DIR__ . '/data/functionPhpDocs-psalmPrefix.php', $description, - $expression + $expression, ); } /** * @dataProvider dataTypeFromFunctionPhpDocs * @dataProvider dataTypeFromFunctionPrefixedPhpDocs - * @param string $description - * @param string $expression */ public function testTypeFromFunctionPhpDocsPhpstanPrefix( string $description, - string $expression + string $expression, ): void { require_once __DIR__ . '/data/functionPhpDocs-phpstanPrefix.php'; $this->assertTypes( __DIR__ . '/data/functionPhpDocs-phpstanPrefix.php', $description, - $expression + $expression, ); } @@ -3855,32 +3803,27 @@ public function dataTypeFromMethodPhpDocs(): array /** * @dataProvider dataTypeFromFunctionPhpDocs * @dataProvider dataTypeFromMethodPhpDocs - * @param string $description - * @param string $expression */ public function testTypeFromMethodPhpDocs( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/methodPhpDocs.php', $description, - $expression + $expression, ); } /** * @dataProvider dataTypeFromFunctionPhpDocs * @dataProvider dataTypeFromMethodPhpDocs - * @param string $description - * @param string $expression - * @param bool $replaceClass */ public function testTypeFromMethodPhpDocsPsalmPrefix( string $description, string $expression, - bool $replaceClass = true + bool $replaceClass = true, ): void { $description = str_replace('static(MethodPhpDocsNamespace\Foo)', 'static(MethodPhpDocsNamespace\FooPsalmPrefix)', $description); @@ -3894,21 +3837,19 @@ public function testTypeFromMethodPhpDocsPsalmPrefix( $this->assertTypes( __DIR__ . '/data/methodPhpDocs-psalmPrefix.php', $description, - $expression + $expression, ); } /** * @dataProvider dataTypeFromFunctionPhpDocs * @dataProvider dataTypeFromMethodPhpDocs - * @param string $description - * @param string $expression * @param bool $replaceClass = true */ public function testTypeFromMethodPhpDocsPhpstanPrefix( string $description, string $expression, - bool $replaceClass = true + bool $replaceClass = true, ): void { $description = str_replace('static(MethodPhpDocsNamespace\Foo)', 'static(MethodPhpDocsNamespace\FooPhpstanPrefix)', $description); @@ -3922,21 +3863,18 @@ public function testTypeFromMethodPhpDocsPhpstanPrefix( $this->assertTypes( __DIR__ . '/data/methodPhpDocs-phpstanPrefix.php', $description, - $expression + $expression, ); } /** * @dataProvider dataTypeFromFunctionPhpDocs * @dataProvider dataTypeFromMethodPhpDocs - * @param string $description - * @param string $expression - * @param bool $replaceClass */ public function testTypeFromTraitPhpDocs( string $description, string $expression, - bool $replaceClass = true + bool $replaceClass = true, ): void { $description = str_replace('static(MethodPhpDocsNamespace\Foo)', 'static(MethodPhpDocsNamespace\FooWithTrait)', $description); @@ -3950,21 +3888,18 @@ public function testTypeFromTraitPhpDocs( $this->assertTypes( __DIR__ . '/data/methodPhpDocs-trait.php', $description, - $expression + $expression, ); } /** * @dataProvider dataTypeFromFunctionPhpDocs * @dataProvider dataTypeFromMethodPhpDocs - * @param string $description - * @param string $expression - * @param bool $replaceClass */ public function testTypeFromMethodPhpDocsInheritDocWithoutCurlyBraces( string $description, string $expression, - bool $replaceClass = true + bool $replaceClass = true, ): void { if ($replaceClass) { @@ -3978,21 +3913,18 @@ public function testTypeFromMethodPhpDocsInheritDocWithoutCurlyBraces( $this->assertTypes( __DIR__ . '/data/method-phpDocs-inheritdoc-without-curly-braces.php', $description, - $expression + $expression, ); } /** * @dataProvider dataTypeFromFunctionPhpDocs * @dataProvider dataTypeFromMethodPhpDocs - * @param string $description - * @param string $expression - * @param bool $replaceClass */ public function testTypeFromRecursiveTraitPhpDocs( string $description, string $expression, - bool $replaceClass = true + bool $replaceClass = true, ): void { $description = str_replace('static(MethodPhpDocsNamespace\Foo)', 'static(MethodPhpDocsNamespace\FooWithRecursiveTrait)', $description); @@ -4006,7 +3938,7 @@ public function testTypeFromRecursiveTraitPhpDocs( $this->assertTypes( __DIR__ . '/data/methodPhpDocs-recursiveTrait.php', $description, - $expression + $expression, ); } @@ -4022,32 +3954,27 @@ public function dataTypeFromTraitPhpDocsInSameFile(): array /** * @dataProvider dataTypeFromTraitPhpDocsInSameFile - * @param string $description - * @param string $expression */ public function testTypeFromTraitPhpDocsInSameFile( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/methodPhpDocs-traitInSameFileAsClass.php', $description, - $expression + $expression, ); } /** * @dataProvider dataTypeFromFunctionPhpDocs * @dataProvider dataTypeFromMethodPhpDocs - * @param string $description - * @param string $expression - * @param bool $replaceClass */ public function testTypeFromMethodPhpDocsInheritDoc( string $description, string $expression, - bool $replaceClass = true + bool $replaceClass = true, ): void { if ($replaceClass) { @@ -4061,21 +3988,18 @@ public function testTypeFromMethodPhpDocsInheritDoc( $this->assertTypes( __DIR__ . '/data/method-phpDocs-inheritdoc.php', $description, - $expression + $expression, ); } /** * @dataProvider dataTypeFromFunctionPhpDocs * @dataProvider dataTypeFromMethodPhpDocs - * @param string $description - * @param string $expression - * @param bool $replaceClass */ public function testTypeFromMethodPhpDocsImplicitInheritance( string $description, string $expression, - bool $replaceClass = true + bool $replaceClass = true, ): void { if ($replaceClass) { @@ -4089,7 +4013,7 @@ public function testTypeFromMethodPhpDocsImplicitInheritance( $this->assertTypes( __DIR__ . '/data/methodPhpDocs-implicitInheritance.php', $description, - $expression + $expression, ); } @@ -4098,7 +4022,7 @@ public function testNotSwitchInstanceof(): void $this->assertTypes( __DIR__ . '/data/switch-instanceof-not.php', '*ERROR*', - '$foo' + '$foo', ); } @@ -4122,35 +4046,31 @@ public function dataSwitchInstanceOf(): array /** * @dataProvider dataSwitchInstanceOf - * @param string $description - * @param string $expression */ public function testSwitchInstanceof( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/switch-instanceof.php', $description, - $expression + $expression, ); } /** * @dataProvider dataSwitchInstanceOf - * @param string $description - * @param string $expression */ public function testSwitchInstanceofTruthy( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/switch-instanceof-truthy.php', $description, - $expression + $expression, ); } @@ -4172,25 +4092,18 @@ public function dataSwitchGetClass(): array /** * @dataProvider dataSwitchGetClass - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression */ public function testSwitchGetClass( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( __DIR__ . '/data/switch-get-class.php', $description, $expression, - [], - [], - [], - [], - $evaluatedPointExpression + $evaluatedPointExpression, ); } @@ -4206,18 +4119,16 @@ public function dataSwitchInstanceOfFallthrough(): array /** * @dataProvider dataSwitchInstanceOfFallthrough - * @param string $description - * @param string $expression */ public function testSwitchInstanceOfFallthrough( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/switch-instanceof-fallthrough.php', $description, - $expression + $expression, ); } @@ -4233,318 +4144,16 @@ public function dataSwitchTypeElimination(): array /** * @dataProvider dataSwitchTypeElimination - * @param string $description - * @param string $expression */ public function testSwitchTypeElimination( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/switch-type-elimination.php', $description, - $expression - ); - } - - public function dataDynamicMethodReturnTypeExtensions(): array - { - return [ - [ - '*ERROR*', - '$em->getByFoo($foo)', - ], - [ - 'DynamicMethodReturnTypesNamespace\Entity', - '$em->getByPrimary()', - ], - [ - 'DynamicMethodReturnTypesNamespace\Entity', - '$em->getByPrimary($foo)', - ], - [ - 'DynamicMethodReturnTypesNamespace\Foo', - '$em->getByPrimary(DynamicMethodReturnTypesNamespace\Foo::class)', - ], - [ - '*ERROR*', - '$iem->getByFoo($foo)', - ], - [ - 'DynamicMethodReturnTypesNamespace\Entity', - '$iem->getByPrimary()', - ], - [ - 'DynamicMethodReturnTypesNamespace\Entity', - '$iem->getByPrimary($foo)', - ], - [ - 'DynamicMethodReturnTypesNamespace\Foo', - '$iem->getByPrimary(DynamicMethodReturnTypesNamespace\Foo::class)', - ], - [ - '*ERROR*', - 'EntityManager::getByFoo($foo)', - ], - [ - 'DynamicMethodReturnTypesNamespace\EntityManager', - '\DynamicMethodReturnTypesNamespace\EntityManager::createManagerForEntity()', - ], - [ - 'DynamicMethodReturnTypesNamespace\EntityManager', - '\DynamicMethodReturnTypesNamespace\EntityManager::createManagerForEntity($foo)', - ], - [ - 'DynamicMethodReturnTypesNamespace\Foo', - '\DynamicMethodReturnTypesNamespace\EntityManager::createManagerForEntity(DynamicMethodReturnTypesNamespace\Foo::class)', - ], - [ - '*ERROR*', - '\DynamicMethodReturnTypesNamespace\InheritedEntityManager::getByFoo($foo)', - ], - [ - 'DynamicMethodReturnTypesNamespace\EntityManager', - '\DynamicMethodReturnTypesNamespace\InheritedEntityManager::createManagerForEntity()', - ], - [ - 'DynamicMethodReturnTypesNamespace\EntityManager', - '\DynamicMethodReturnTypesNamespace\InheritedEntityManager::createManagerForEntity($foo)', - ], - [ - 'DynamicMethodReturnTypesNamespace\Foo', - '\DynamicMethodReturnTypesNamespace\InheritedEntityManager::createManagerForEntity(DynamicMethodReturnTypesNamespace\Foo::class)', - ], - [ - 'DynamicMethodReturnTypesNamespace\Foo', - '$container[\DynamicMethodReturnTypesNamespace\Foo::class]', - ], - [ - 'object', - 'new \DynamicMethodReturnTypesNamespace\Foo()', - ], - [ - 'object', - 'new \DynamicMethodReturnTypesNamespace\FooWithoutConstructor()', - ], - ]; - } - - /** - * @dataProvider dataDynamicMethodReturnTypeExtensions - * @param string $description - * @param string $expression - */ - public function testDynamicMethodReturnTypeExtensions( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/dynamic-method-return-types.php', - $description, - $expression, - [ - new class() implements DynamicMethodReturnTypeExtension { - - public function getClass(): string - { - return \DynamicMethodReturnTypesNamespace\EntityManager::class; - } - - public function isMethodSupported(MethodReflection $methodReflection): bool - { - return in_array($methodReflection->getName(), ['getByPrimary'], true); - } - - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): \PHPStan\Type\Type - { - $args = $methodCall->args; - if (count($args) === 0) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - } - - $arg = $args[0]->value; - if (!($arg instanceof \PhpParser\Node\Expr\ClassConstFetch)) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - } - - if (!($arg->class instanceof \PhpParser\Node\Name)) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - } - - return new ObjectType((string) $arg->class); - } - - }, - new class() implements DynamicMethodReturnTypeExtension { - - public function getClass(): string - { - return \DynamicMethodReturnTypesNamespace\ComponentContainer::class; - } - - public function isMethodSupported(MethodReflection $methodReflection): bool - { - return $methodReflection->getName() === 'offsetGet'; - } - - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type - { - $args = $methodCall->args; - if (count($args) === 0) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - } - - $argType = $scope->getType($args[0]->value); - if (!$argType instanceof ConstantStringType) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - } - - return new ObjectType($argType->getValue()); - } - - }, - ], - [ - new class() implements DynamicStaticMethodReturnTypeExtension { - - public function getClass(): string - { - return \DynamicMethodReturnTypesNamespace\EntityManager::class; - } - - public function isStaticMethodSupported(MethodReflection $methodReflection): bool - { - return in_array($methodReflection->getName(), ['createManagerForEntity'], true); - } - - public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): \PHPStan\Type\Type - { - $args = $methodCall->args; - if (count($args) === 0) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - } - - $arg = $args[0]->value; - if (!($arg instanceof \PhpParser\Node\Expr\ClassConstFetch)) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - } - - if (!($arg->class instanceof \PhpParser\Node\Name)) { - return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - } - - return new ObjectType((string) $arg->class); - } - - }, - new class() implements DynamicStaticMethodReturnTypeExtension { - - public function getClass(): string - { - return \DynamicMethodReturnTypesNamespace\Foo::class; - } - - public function isStaticMethodSupported(MethodReflection $methodReflection): bool - { - return $methodReflection->getName() === '__construct'; - } - - public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): \PHPStan\Type\Type - { - return new ObjectWithoutClassType(); - } - - }, - new class() implements DynamicStaticMethodReturnTypeExtension { - - public function getClass(): string - { - return \DynamicMethodReturnTypesNamespace\FooWithoutConstructor::class; - } - - public function isStaticMethodSupported(MethodReflection $methodReflection): bool - { - return $methodReflection->getName() === '__construct'; - } - - public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): \PHPStan\Type\Type - { - return new ObjectWithoutClassType(); - } - - }, - ] - ); - } - - public function dataDynamicReturnTypeExtensionsOnCompoundTypes(): array - { - return [ - [ - 'DynamicMethodReturnCompoundTypes\Collection', - '$collection->getSelf()', - ], - [ - 'DynamicMethodReturnCompoundTypes\Collection|DynamicMethodReturnCompoundTypes\Foo', - '$collectionOrFoo->getSelf()', - ], - ]; - } - - /** - * @dataProvider dataDynamicReturnTypeExtensionsOnCompoundTypes - * @param string $description - * @param string $expression - */ - public function testDynamicReturnTypeExtensionsOnCompoundTypes( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/dynamic-method-return-compound-types.php', - $description, $expression, - [ - new class () implements DynamicMethodReturnTypeExtension { - - public function getClass(): string - { - return \DynamicMethodReturnCompoundTypes\Collection::class; - } - - public function isMethodSupported(MethodReflection $methodReflection): bool - { - return $methodReflection->getName() === 'getSelf'; - } - - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type - { - return new ObjectType(\DynamicMethodReturnCompoundTypes\Collection::class); - } - - }, - new class () implements DynamicMethodReturnTypeExtension { - - public function getClass(): string - { - return \DynamicMethodReturnCompoundTypes\Foo::class; - } - - public function isMethodSupported(MethodReflection $methodReflection): bool - { - return $methodReflection->getName() === 'getSelf'; - } - - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type - { - return new ObjectType(\DynamicMethodReturnCompoundTypes\Foo::class); - } - - }, - ] ); } @@ -4571,25 +4180,18 @@ public function dataOverwritingVariable(): array /** * @dataProvider dataOverwritingVariable - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpressionType */ public function testOverwritingVariable( string $description, string $expression, - string $evaluatedPointExpressionType + string $evaluatedPointExpressionType, ): void { $this->assertTypes( __DIR__ . '/data/overwritingVariable.php', $description, $expression, - [], - [], - [], - [], - $evaluatedPointExpressionType + $evaluatedPointExpressionType, ); } @@ -4645,18 +4247,16 @@ public function dataNegatedInstanceof(): array /** * @dataProvider dataNegatedInstanceof - * @param string $description - * @param string $expression */ public function testNegatedInstanceof( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/negated-instanceof.php', $description, - $expression + $expression, ); } @@ -4668,7 +4268,7 @@ public function dataAnonymousFunction(): array '$str', ], [ - 'array', + PHP_VERSION_ID < 80000 ? 'array' : 'array', '$arr', ], [ @@ -4684,18 +4284,16 @@ public function dataAnonymousFunction(): array /** * @dataProvider dataAnonymousFunction - * @param string $description - * @param string $expression */ public function testAnonymousFunction( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/anonymous-function.php', $description, - $expression + $expression, ); } @@ -4779,7 +4377,7 @@ public function dataForeachArrayType(): array ], [ __DIR__ . '/data/foreach/foreach-with-specified-key-type.php', - 'array&nonEmpty', + 'non-empty-array', '$list', ], [ @@ -4822,20 +4420,17 @@ public function dataForeachArrayType(): array /** * @dataProvider dataForeachArrayType - * @param string $file - * @param string $description - * @param string $expression */ public function testForeachArrayType( string $file, string $description, - string $expression + string $expression, ): void { $this->assertTypes( $file, $description, - $expression + $expression, ); } @@ -4852,20 +4447,17 @@ public function dataOverridingSpecifiedType(): array /** * @dataProvider dataOverridingSpecifiedType - * @param string $file - * @param string $description - * @param string $expression */ public function testOverridingSpecifiedType( string $file, string $description, - string $expression + string $expression, ): void { $this->assertTypes( $file, $description, - $expression + $expression, ); } @@ -4913,27 +4505,19 @@ public function dataForeachObjectType(): array /** * @dataProvider dataForeachObjectType - * @param string $file - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression */ public function testForeachObjectType( string $file, string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( $file, $description, $expression, - [], - [], - [], - [], - $evaluatedPointExpression + $evaluatedPointExpression, ); } @@ -4945,7 +4529,7 @@ public function dataArrayFunctions(): array '$integers[0]', ], [ - 'array(string, string, string)', + 'array{string, string, string}', '$mappedStrings', ], [ @@ -4997,7 +4581,7 @@ public function dataArrayFunctions(): array 'array_combine($array, $array2)', ], [ - 'array(1 => 2)', + 'array{1: 2}', 'array_combine([1], [2])', ], [ @@ -5005,7 +4589,7 @@ public function dataArrayFunctions(): array 'array_combine([1, 2], [3])', ], [ - 'array(\'a\' => \'d\', \'b\' => \'e\', \'c\' => \'f\')', + 'array{a: \'d\', b: \'e\', c: \'f\'}', 'array_combine([\'a\', \'b\', \'c\'], [\'d\', \'e\', \'f\'])', ], [ @@ -5097,23 +4681,47 @@ public function dataArrayFunctions(): array 'array_uintersect($integers, [])', ], [ - 'array(1, 1, 1, 1, 1)', + 'array{1, 1, 1, 1, 1}', '$filledIntegers', ], [ - 'array(1)', + 'array{}', + '$emptyFilled', + ], + [ + 'array{1}', '$filledIntegersWithKeys', ], [ - 'array(1, 2)', + 'non-empty-array', + '$filledNonEmptyArray', + ], + [ + PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*', + '$filledAlwaysFalse', + ], + [ + PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*', + '$filledNegativeConstAlwaysFalse', + ], + [ + 'array|false', + '$filledByMaybeNegativeRange', + ], + [ + 'non-empty-array', + '$filledByPositiveRange', + ], + [ + 'array{1, 2}', 'array_keys($integerKeys)', ], [ - 'array(\'foo\', \'bar\')', + 'array{\'foo\', \'bar\'}', 'array_keys($stringKeys)', ], [ - 'array(\'foo\', 1)', + 'array{\'foo\', 1}', 'array_keys($stringOrIntegerKeys)', ], [ @@ -5121,7 +4729,7 @@ public function dataArrayFunctions(): array 'array_keys($generalStringKeys)', ], [ - 'array(\'foo\', stdClass)', + 'array{\'foo\', stdClass}', 'array_values($integerKeys)', ], [ @@ -5129,7 +4737,7 @@ public function dataArrayFunctions(): array 'array_values($generalStringKeys)', ], [ - 'array', + 'non-empty-array', 'array_merge($stringOrIntegerKeys)', ], [ @@ -5137,23 +4745,23 @@ public function dataArrayFunctions(): array 'array_merge($generalStringKeys, $generalDateTimeValues)', ], [ - 'array', + 'non-empty-array', 'array_merge($generalStringKeys, $stringOrIntegerKeys)', ], [ - 'array', + 'non-empty-array', 'array_merge($stringOrIntegerKeys, $generalStringKeys)', ], [ - 'array', + 'non-empty-array', 'array_merge($stringKeys, $stringOrIntegerKeys)', ], [ - 'array', + 'non-empty-array', 'array_merge($stringOrIntegerKeys, $stringKeys)', ], [ - 'array', + 'non-empty-array', 'array_merge(array("color" => "red", 2, 4), array("a", "b", "color" => "green", "shape" => "trapezoid", 4))', ], [ @@ -5165,19 +4773,19 @@ public function dataArrayFunctions(): array '$mergedInts', ], [ - 'array(5 => \'banana\', 6 => \'banana\', 7 => \'banana\', 8 => \'banana\', 9 => \'banana\', 10 => \'banana\')', + 'array{5: \'banana\', 6: \'banana\', 7: \'banana\', 8: \'banana\', 9: \'banana\', 10: \'banana\'}', 'array_fill(5, 6, \'banana\')', ], [ - 'array&nonEmpty', + 'non-empty-array', 'array_fill(0, 101, \'apple\')', ], [ - 'array(-2 => \'pear\', 0 => \'pear\', 1 => \'pear\', 2 => \'pear\')', + 'array{-2: \'pear\', 0: \'pear\', 1: \'pear\', 2: \'pear\'}', 'array_fill(-2, 4, \'pear\')', ], [ - 'array&nonEmpty', + 'non-empty-array', 'array_fill($integer, 2, new \stdClass())', ], [ @@ -5189,7 +4797,7 @@ public function dataArrayFunctions(): array 'array_fill_keys($generalStringKeys, new \stdClass())', ], [ - 'array(\'foo\' => \'banana\', 5 => \'banana\', 10 => \'banana\', \'bar\' => \'banana\')', + 'array{foo: \'banana\', 5: \'banana\', 10: \'banana\', bar: \'banana\'}', 'array_fill_keys([\'foo\', 5, 10, \'bar\'], \'banana\')', ], [ @@ -5209,11 +4817,11 @@ public function dataArrayFunctions(): array '$unknownArray', ], [ - 'array(\'foo\' => \'banana\', \'bar\' => \'banana\', ?\'baz\' => \'banana\', ?\'lorem\' => \'banana\')', + 'array{foo: \'banana\', bar: \'banana\', baz?: \'banana\', lorem?: \'banana\'}', 'array_fill_keys($conditionalArray, \'banana\')', ], [ - 'array(\'foo\' => stdClass, \'bar\' => stdClass, ?\'baz\' => stdClass, ?\'lorem\' => stdClass)', + 'array{foo: stdClass, bar: stdClass, baz?: stdClass, lorem?: stdClass}', 'array_map(function (): \stdClass {}, $conditionalKeysArray)', ], [ @@ -5249,11 +4857,11 @@ public function dataArrayFunctions(): array 'array_shift([])', ], [ - 'array(null, \'\', 1)', + 'array{null, \'\', 1}', '$constantArrayWithFalseyValues', ], [ - 'array(2 => 1)', + 'array{2: 1}', '$constantTruthyValues', ], [ @@ -5261,7 +4869,7 @@ public function dataArrayFunctions(): array '$falsey', ], [ - 'array()', + 'array{}', 'array_filter($falsey)', ], [ @@ -5273,11 +4881,11 @@ public function dataArrayFunctions(): array 'array_filter($withFalsey)', ], [ - 'array(\'a\' => 1)', + 'array{a: 1}', 'array_filter($union)', ], [ - 'array(?0 => true, ?1 => int|int<1, max>)', + 'array{0?: true, 1?: int|int<1, max>}', 'array_filter($withPossiblyFalsey)', ], [ @@ -5465,55 +5073,55 @@ public function dataArrayFunctions(): array 'array_slice($unknownArray, -2, 1, true)', ], [ - 'array(0 => bool, 1 => int, 2 => \'\', \'a\' => 0)', + 'array{0: bool, 1: int, 2: \'\', a: 0}', 'array_slice($withPossiblyFalsey, 0)', ], [ - 'array(0 => int, 1 => \'\', \'a\' => 0)', + 'array{0: int, 1: \'\', a: 0}', 'array_slice($withPossiblyFalsey, 1)', ], [ - 'array(1 => int, 2 => \'\', \'a\' => 0)', + 'array{1: int, 2: \'\', a: 0}', 'array_slice($withPossiblyFalsey, 1, null, true)', ], [ - 'array(0 => \'\', \'a\' => 0)', + 'array{0: \'\', a: 0}', 'array_slice($withPossiblyFalsey, 2, 3)', ], [ - 'array(2 => \'\', \'a\' => 0)', + 'array{2: \'\', a: 0}', 'array_slice($withPossiblyFalsey, 2, 3, true)', ], [ - 'array(int, \'\')', + 'array{int, \'\'}', 'array_slice($withPossiblyFalsey, 1, -1)', ], [ - 'array(1 => int, 2 => \'\')', + 'array{1: int, 2: \'\'}', 'array_slice($withPossiblyFalsey, 1, -1, true)', ], [ - 'array(0 => \'\', \'a\' => 0)', + 'array{0: \'\', a: 0}', 'array_slice($withPossiblyFalsey, -2, null)', ], [ - 'array(2 => \'\', \'a\' => 0)', + 'array{2: \'\', a: 0}', 'array_slice($withPossiblyFalsey, -2, null, true)', ], [ - 'array(\'baz\' => \'qux\')|array(0 => \'\', \'a\' => 0)', + 'array{0: \'\', a: 0}|array{baz: \'qux\'}', 'array_slice($unionArrays, 1)', ], [ - 'array(\'a\' => 0)|array(\'baz\' => \'qux\')', + 'array{a: 0}|array{baz: \'qux\'}', 'array_slice($unionArrays, -1, null, true)', ], [ - 'array(0 => \'foo\', 1 => \'bar\', \'baz\' => \'qux\', 2 => \'quux\', \'quuz\' => \'corge\', 3 => \'grault\')', + 'array{0: \'foo\', 1: \'bar\', baz: \'qux\', 2: \'quux\', quuz: \'corge\', 3: \'grault\'}', '$slicedOffset', ], [ - 'array(4 => \'foo\', 1 => \'bar\', \'baz\' => \'qux\', 0 => \'quux\', \'quuz\' => \'corge\', 5 => \'grault\')', + 'array{4: \'foo\', 1: \'bar\', baz: \'qux\', 0: \'quux\', quuz: \'corge\', 5: \'grault\'}', '$slicedOffsetWithKeys', ], [ @@ -5605,18 +5213,16 @@ public function dataArrayFunctions(): array /** * @dataProvider dataArrayFunctions - * @param string $description - * @param string $expression */ public function testArrayFunctions( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/array-functions.php', $description, - $expression + $expression, ); } @@ -5643,26 +5249,6 @@ public function dataFunctions(): array '(float|string)', '$microtimeBenevolent', ], - [ - 'int', - '$strtotimeNow', - ], - [ - 'false', - '$strtotimeInvalid', - ], - [ - 'int|false', - '$strtotimeUnknown', - ], - [ - '(int|false)', - '$strtotimeUnknown2', - ], - [ - 'int|false', - '$strtotimeCrash', - ], [ '-1', '$versionCompare1', @@ -5696,27 +5282,27 @@ public function dataFunctions(): array '$versionCompare8', ], [ - 'int', + 'int<0, max>', '$mbStrlenWithoutEncoding', ], [ - 'int', + 'int<0, max>', '$mbStrlenWithValidEncoding', ], [ - 'int', + 'int<0, max>', '$mbStrlenWithValidEncodingAlias', ], [ - 'false', + PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*', '$mbStrlenWithInvalidEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'int|false' : 'int', + PHP_VERSION_ID < 80000 ? 'int<0, max>|false' : 'int<0, max>', '$mbStrlenWithValidAndInvalidEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'int|false' : 'int', + PHP_VERSION_ID < 80000 ? 'int<0, max>|false' : 'int<0, max>', '$mbStrlenWithUnknownEncoding', ], [ @@ -5784,7 +5370,7 @@ public function dataFunctions(): array '$mbEncodingAliasesWithValidEncoding', ], [ - 'false', + PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*', '$mbEncodingAliasesWithInvalidEncoding', ], [ @@ -5804,7 +5390,7 @@ public function dataFunctions(): array '$mbChrWithValidEncoding', ], [ - 'false', + PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*', '$mbChrWithInvalidEncoding', ], [ @@ -5824,7 +5410,7 @@ public function dataFunctions(): array '$mbOrdWithValidEncoding', ], [ - 'false', + PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*', '$mbOrdWithInvalidEncoding', ], [ @@ -5836,11 +5422,11 @@ public function dataFunctions(): array '$mbOrdWithUnknownEncoding', ], [ - 'array(\'sec\' => int, \'usec\' => int, \'minuteswest\' => int, \'dsttime\' => int)', + 'array{sec: int, usec: int, minuteswest: int, dsttime: int}', '$gettimeofdayArrayWithoutArg', ], [ - 'array(\'sec\' => int, \'usec\' => int, \'minuteswest\' => int, \'dsttime\' => int)', + 'array{sec: int, usec: int, minuteswest: int, dsttime: int}', '$gettimeofdayArray', ], [ @@ -5848,31 +5434,31 @@ public function dataFunctions(): array '$gettimeofdayFloat', ], [ - 'array(\'sec\' => int, \'usec\' => int, \'minuteswest\' => int, \'dsttime\' => int)|float', + 'array{sec: int, usec: int, minuteswest: int, dsttime: int}|float', '$gettimeofdayDefault', ], [ - '(array(\'sec\' => int, \'usec\' => int, \'minuteswest\' => int, \'dsttime\' => int)|float)', + '(array{sec: int, usec: int, minuteswest: int, dsttime: int}|float)', '$gettimeofdayBenevolent', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', '$strSplitConstantStringWithoutDefinedParameters', ], [ - "array('a', 'b', 'c', 'd', 'e', 'f')", + 'array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', '$strSplitConstantStringWithoutDefinedSplitLength', ], [ - 'array', + 'non-empty-array', '$strSplitStringWithoutDefinedSplitLength', ], [ - "array('a', 'b', 'c', 'd', 'e', 'f')", + 'array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', '$strSplitConstantStringWithOneSplitLength', ], [ - "array('abcdef')", + 'array{\'abcdef\'}', '$strSplitConstantStringWithGreaterSplitLengthThanStringLength', ], [ @@ -5880,32 +5466,32 @@ public function dataFunctions(): array '$strSplitConstantStringWithFailureSplitLength', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', '$strSplitConstantStringWithInvalidSplitLengthType', ], [ - 'array', + 'non-empty-array', '$strSplitConstantStringWithVariableStringAndConstantSplitLength', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', '$strSplitConstantStringWithVariableStringAndVariableSplitLength', ], // parse_url [ - 'mixed', + 'array|int|string|false|null', '$parseUrlWithoutParameters', ], [ - "array('scheme' => 'http', 'host' => 'abc.def')", + 'array{scheme: \'http\', host: \'abc.def\'}', '$parseUrlConstantUrlWithoutComponent1', ], [ - "array('scheme' => 'http', 'host' => 'def.abc')", + 'array{scheme: \'http\', host: \'def.abc\'}', '$parseUrlConstantUrlWithoutComponent2', ], [ - "array(?'scheme' => string, ?'host' => string, ?'port' => int, ?'user' => string, ?'pass' => string, ?'path' => string, ?'query' => string, ?'fragment' => string)|false", + 'array{scheme?: string, host?: string, port?: int, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', '$parseUrlConstantUrlUnknownComponent', ], [ @@ -5929,11 +5515,11 @@ public function dataFunctions(): array '$parseUrlStringUrlWithComponentPort', ], [ - "array(?'scheme' => string, ?'host' => string, ?'port' => int, ?'user' => string, ?'pass' => string, ?'path' => string, ?'query' => string, ?'fragment' => string)|false", + 'array{scheme?: string, host?: string, port?: int, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', '$parseUrlStringUrlWithoutComponent', ], [ - "array('path' => 'abc.def')", + 'array{path: \'abc.def\'}', "parse_url('abc.def')", ], [ @@ -5945,17 +5531,21 @@ public function dataFunctions(): array "parse_url('http://abc.def', PHP_URL_SCHEME)", ], [ - 'array(0 => int, 1 => int, 2 => int, 3 => int, 4 => int, 5 => int, 6 => int, 7 => int, 8 => int, 9 => int, 10 => int, 11 => int, 12 => int, \'dev\' => int, \'ino\' => int, \'mode\' => int, \'nlink\' => int, \'uid\' => int, \'gid\' => int, \'rdev\' => int, \'size\' => int, \'atime\' => int, \'mtime\' => int, \'ctime\' => int, \'blksize\' => int, \'blocks\' => int)|false', + 'array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', '$stat', ], [ - 'array(0 => int, 1 => int, 2 => int, 3 => int, 4 => int, 5 => int, 6 => int, 7 => int, 8 => int, 9 => int, 10 => int, 11 => int, 12 => int, \'dev\' => int, \'ino\' => int, \'mode\' => int, \'nlink\' => int, \'uid\' => int, \'gid\' => int, \'rdev\' => int, \'size\' => int, \'atime\' => int, \'mtime\' => int, \'ctime\' => int, \'blksize\' => int, \'blocks\' => int)|false', + 'array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', '$lstat', ], [ - 'array(0 => int, 1 => int, 2 => int, 3 => int, 4 => int, 5 => int, 6 => int, 7 => int, 8 => int, 9 => int, 10 => int, 11 => int, 12 => int, \'dev\' => int, \'ino\' => int, \'mode\' => int, \'nlink\' => int, \'uid\' => int, \'gid\' => int, \'rdev\' => int, \'size\' => int, \'atime\' => int, \'mtime\' => int, \'ctime\' => int, \'blksize\' => int, \'blocks\' => int)|false', + 'array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', '$fstat', ], + [ + 'array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}', + '$fileObjectStat', + ], [ 'string', '$base64DecodeWithoutStrict', @@ -6012,79 +5602,21 @@ public function dataFunctions(): array 'array|int|false', '$strWordCountStrTypeIndeterminant', ], - [ - 'string', - '$hashHmacMd5', - ], - [ - 'string', - '$hashHmacSha256', - ], - [ - 'false', - '$hashHmacNonCryptographic', - ], - [ - 'false', - '$hashHmacRandom', - ], - [ - 'string', - '$hashHmacVariable', - ], - [ - 'string|false', - '$hashHmacFileMd5', - ], - [ - 'string|false', - '$hashHmacFileSha256', - ], - [ - 'false', - '$hashHmacFileNonCryptographic', - ], - [ - 'false', - '$hashHmacFileRandom', - ], - [ - '(string|false)', - '$hashHmacFileVariable', - ], - [ - 'string', - '$hash', - ], - [ - 'string', - '$hashRaw', - ], - [ - 'false', - '$hashRandom', - ], - [ - 'string', - '$hashMixed', - ], ]; } /** * @dataProvider dataFunctions - * @param string $description - * @param string $expression */ public function testFunctions( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/functions.php', $description, - $expression + $expression, ); } @@ -6092,7 +5624,7 @@ public function dataDioFunctions(): array { return [ [ - 'array(\'device\' => int, \'inode\' => int, \'mode\' => int, \'nlink\' => int, \'uid\' => int, \'gid\' => int, \'device_type\' => int, \'size\' => int, \'blocksize\' => int, \'blocks\' => int, \'atime\' => int, \'mtime\' => int, \'ctime\' => int)|null', + 'array{device: int, inode: int, mode: int, nlink: int, uid: int, gid: int, device_type: int, size: int, blocksize: int, blocks: int, atime: int, mtime: int, ctime: int}|null', '$stat', ], ]; @@ -6100,12 +5632,10 @@ public function dataDioFunctions(): array /** * @dataProvider dataDioFunctions - * @param string $description - * @param string $expression */ public function testDioFunctions( string $description, - string $expression + string $expression, ): void { if (!function_exists('dio_stat')) { @@ -6114,7 +5644,7 @@ public function testDioFunctions( $this->assertTypes( __DIR__ . '/data/dio-functions.php', $description, - $expression + $expression, ); } @@ -6122,7 +5652,7 @@ public function dataSsh2Functions(): array { return [ [ - 'array(0 => int, 1 => int, 2 => int, 3 => int, 4 => int, 5 => int, 6 => int, 7 => int, 8 => int, 9 => int, 10 => int, 11 => int, 12 => int, \'dev\' => int, \'ino\' => int, \'mode\' => int, \'nlink\' => int, \'uid\' => int, \'gid\' => int, \'rdev\' => int, \'size\' => int, \'atime\' => int, \'mtime\' => int, \'ctime\' => int, \'blksize\' => int, \'blocks\' => int)|false', + 'array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int, 6: int, 7: int, 8: int, 9: int, 10: int, 11: int, 12: int, dev: int, ino: int, mode: int, nlink: int, uid: int, gid: int, rdev: int, size: int, atime: int, mtime: int, ctime: int, blksize: int, blocks: int}|false', '$ssh2SftpStat', ], ]; @@ -6130,18 +5660,16 @@ public function dataSsh2Functions(): array /** * @dataProvider dataSsh2Functions - * @param string $description - * @param string $expression */ public function testSsh2Functions( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/ssh2-functions.php', $description, - $expression + $expression, ); } @@ -6149,19 +5677,19 @@ public function dataRangeFunction(): array { return [ [ - 'array(2, 3, 4, 5)', + 'array{2, 3, 4, 5}', 'range(2, 5)', ], [ - 'array(2, 4)', + 'array{2, 4}', 'range(2, 5, 2)', ], [ - 'array(2.0, 3.0, 4.0, 5.0)', + 'array{2.0, 3.0, 4.0, 5.0}', 'range(2, 5, 1.0)', ], [ - 'array(2.1, 3.1, 4.1)', + 'array{2.1, 3.1, 4.1}', 'range(2.1, 5)', ], [ @@ -6181,19 +5709,19 @@ public function dataRangeFunction(): array 'range($integer, $mixed)', ], [ - 'array(0 => 1, ?1 => 2)', + 'array{0: 1, 1?: 2}', 'range(1, doFoo() ? 1 : 2)', ], [ - 'array(0 => -1|1, ?1 => 0|2, ?2 => 1, ?3 => 2)', + 'array{0: -1|1, 1?: 0|2, 2?: 1, 3?: 2}', 'range(doFoo() ? -1 : 1, doFoo() ? 1 : 2)', ], [ - 'array(3, 2, 1, 0, -1)', + 'array{3, 2, 1, 0, -1}', 'range(3, -1)', ], [ - 'array&nonEmpty', + 'non-empty-array>', 'range(0, 50)', ], ]; @@ -6201,18 +5729,16 @@ public function dataRangeFunction(): array /** * @dataProvider dataRangeFunction - * @param string $description - * @param string $expression */ public function testRangeFunction( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/range-function.php', $description, - $expression + $expression, ); } @@ -6360,201 +5886,16 @@ public function dataSpecifiedTypesUsingIsFunctions(): array /** * @dataProvider dataSpecifiedTypesUsingIsFunctions - * @param string $description - * @param string $expression */ public function testSpecifiedTypesUsingIsFunctions( - string $description, - string $expression - ): void - { - $this->assertTypes( - __DIR__ . '/data/specifiedTypesUsingIsFunctions.php', - $description, - $expression - ); - } - - public function dataTypeSpecifyingExtensions(): array - { - return [ - [ - 'string', - '$foo', - true, - ], - [ - 'int', - '$bar', - true, - ], - [ - 'string|null', - '$foo', - false, - ], - [ - 'int|null', - '$bar', - false, - ], - [ - 'string', - '$foo', - null, - ], - [ - 'int', - '$bar', - null, - ], - ]; - } - - /** - * @dataProvider dataTypeSpecifyingExtensions - * @param string $description - * @param string $expression - * @param bool|null $nullContext - */ - public function testTypeSpecifyingExtensions( - string $description, - string $expression, - ?bool $nullContext - ): void - { - $this->assertTypes( - __DIR__ . '/data/type-specifying-extensions.php', - $description, - $expression, - [], - [], - [new AssertionClassMethodTypeSpecifyingExtension($nullContext)], - [new AssertionClassStaticMethodTypeSpecifyingExtension($nullContext)], - 'die', - [], - false - ); - } - - public function dataTypeSpecifyingExtensions2(): array - { - return [ - [ - 'string|null', - '$foo', - true, - ], - [ - 'int|null', - '$bar', - true, - ], - [ - 'string|null', - '$foo', - false, - ], - [ - 'int|null', - '$bar', - false, - ], - [ - 'string|null', - '$foo', - null, - ], - [ - 'int|null', - '$bar', - null, - ], - ]; - } - - /** - * @dataProvider dataTypeSpecifyingExtensions2 - * @param string $description - * @param string $expression - * @param bool|null $nullContext - */ - public function testTypeSpecifyingExtensions2( - string $description, - string $expression, - ?bool $nullContext - ): void - { - $this->assertTypes( - __DIR__ . '/data/type-specifying-extensions2.php', - $description, - $expression, - [], - [], - [new AssertionClassMethodTypeSpecifyingExtension($nullContext)], - [new AssertionClassStaticMethodTypeSpecifyingExtension($nullContext)] - ); - } - - public function dataTypeSpecifyingExtensions3(): array - { - return [ - [ - 'string', - '$foo', - false, - ], - [ - 'int', - '$bar', - false, - ], - [ - 'string|null', - '$foo', - true, - ], - [ - 'int|null', - '$bar', - true, - ], - [ - 'string', - '$foo', - null, - ], - [ - 'int', - '$bar', - null, - ], - ]; - } - - /** - * @dataProvider dataTypeSpecifyingExtensions3 - * @param string $description - * @param string $expression - * @param bool|null $nullContext - */ - public function testTypeSpecifyingExtensions3( string $description, string $expression, - ?bool $nullContext ): void { $this->assertTypes( - __DIR__ . '/data/type-specifying-extensions3.php', + __DIR__ . '/data/specifiedTypesUsingIsFunctions.php', $description, $expression, - [], - [], - [new AssertionClassMethodTypeSpecifyingExtension($nullContext)], - [new AssertionClassStaticMethodTypeSpecifyingExtension($nullContext)], - 'die', - [], - false ); } @@ -6630,7 +5971,7 @@ public function dataIterable(): array '$unionBar', ], [ - 'array&nonEmpty', + 'non-empty-array', '$mixedUnionIterableType', ], [ @@ -6710,18 +6051,16 @@ public function dataIterable(): array /** * @dataProvider dataIterable - * @param string $description - * @param string $expression */ public function testIterable( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/iterable.php', $description, - $expression + $expression, ); } @@ -6749,18 +6088,16 @@ public function dataArrayAccess(): array /** * @dataProvider dataArrayAccess - * @param string $description - * @param string $expression */ public function testArrayAccess( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/array-accessable.php', $description, - $expression + $expression, ); } @@ -6784,18 +6121,16 @@ public function dataVoid(): array /** * @dataProvider dataVoid - * @param string $description - * @param string $expression */ public function testVoid( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/void.php', $description, - $expression + $expression, ); } @@ -6823,18 +6158,16 @@ public function dataNullableReturnTypes(): array /** * @dataProvider dataNullableReturnTypes - * @param string $description - * @param string $expression */ public function testNullableReturnTypes( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/nullable-returnTypes.php', $description, - $expression + $expression, ); } @@ -6870,18 +6203,16 @@ public function dataTernary(): array /** * @dataProvider dataTernary - * @param string $description - * @param string $expression */ public function testTernary( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/ternary.php', $description, - $expression + $expression, ); } @@ -6901,18 +6232,16 @@ public function dataHeredoc(): array /** * @dataProvider dataHeredoc - * @param string $description - * @param string $expression */ public function testHeredoc( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/heredoc.php', $description, - $expression + $expression, ); } @@ -7074,25 +6403,18 @@ public function dataTypeElimination(): array /** * @dataProvider dataTypeElimination - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression */ public function testTypeElimination( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( __DIR__ . '/data/type-elimination.php', $description, $expression, - [], - [], - [], - [], - $evaluatedPointExpression + $evaluatedPointExpression, ); } @@ -7116,18 +6438,16 @@ public function dataMisleadingTypes(): array /** * @dataProvider dataMisleadingTypes - * @param string $description - * @param string $expression */ public function testMisleadingTypes( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/misleading-types.php', $description, - $expression + $expression, ); } @@ -7147,18 +6467,16 @@ public function dataMisleadingTypesWithoutNamespace(): array /** * @dataProvider dataMisleadingTypesWithoutNamespace - * @param string $description - * @param string $expression */ public function testMisleadingTypesWithoutNamespace( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/misleading-types-without-namespace.php', $description, - $expression + $expression, ); } @@ -7182,18 +6500,16 @@ public function dataUnresolvableTypes(): array /** * @dataProvider dataUnresolvableTypes - * @param string $description - * @param string $expression */ public function testUnresolvableTypes( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/unresolvable-types.php', $description, - $expression + $expression, ); } @@ -7213,18 +6529,16 @@ public function dataCombineTypes(): array /** * @dataProvider dataCombineTypes - * @param string $description - * @param string $expression */ public function testCombineTypes( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/combine-types.php', $description, - $expression + $expression, ); } @@ -7254,18 +6568,16 @@ public function dataConstants(): array /** * @dataProvider dataConstants - * @param string $description - * @param string $expression */ public function testConstants( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/constants.php', $description, - $expression + $expression, ); } @@ -7285,35 +6597,31 @@ public function dataFinally(): array /** * @dataProvider dataFinally - * @param string $description - * @param string $expression */ public function testFinally( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/finally.php', $description, - $expression + $expression, ); } /** * @dataProvider dataFinally - * @param string $description - * @param string $expression */ public function testFinallyWithEarlyTermination( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/finally-with-early-termination.php', $description, - $expression + $expression, ); } @@ -7329,35 +6637,31 @@ public function dataInheritDocFromInterface(): array /** * @dataProvider dataInheritDocFromInterface - * @param string $description - * @param string $expression */ public function testInheritDocFromInterface( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/inheritdoc-from-interface.php', $description, - $expression + $expression, ); } /** * @dataProvider dataInheritDocFromInterface - * @param string $description - * @param string $expression */ public function testInheritDocWithoutCurlyBracesFromInterface( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/inheritdoc-without-curly-braces-from-interface.php', $description, - $expression + $expression, ); } @@ -7373,37 +6677,33 @@ public function dataInheritDocFromInterface2(): array /** * @dataProvider dataInheritDocFromInterface2 - * @param string $description - * @param string $expression */ public function testInheritDocFromInterface2( string $description, - string $expression + string $expression, ): void { require_once __DIR__ . '/data/inheritdoc-from-interface2-definition.php'; $this->assertTypes( __DIR__ . '/data/inheritdoc-from-interface2.php', $description, - $expression + $expression, ); } /** * @dataProvider dataInheritDocFromInterface2 - * @param string $description - * @param string $expression */ public function testInheritDocWithoutCurlyBracesFromInterface2( string $description, - string $expression + string $expression, ): void { require_once __DIR__ . '/data/inheritdoc-without-curly-braces-from-interface2-definition.php'; $this->assertTypes( __DIR__ . '/data/inheritdoc-without-curly-braces-from-interface2.php', $description, - $expression + $expression, ); } @@ -7419,35 +6719,31 @@ public function dataInheritDocFromTrait(): array /** * @dataProvider dataInheritDocFromTrait - * @param string $description - * @param string $expression */ public function testInheritDocFromTrait( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/inheritdoc-from-trait.php', $description, - $expression + $expression, ); } /** * @dataProvider dataInheritDocFromTrait - * @param string $description - * @param string $expression */ public function testInheritDocWithoutCurlyBracesFromTrait( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/inheritdoc-without-curly-braces-from-trait.php', $description, - $expression + $expression, ); } @@ -7463,12 +6759,10 @@ public function dataInheritDocFromTrait2(): array /** * @dataProvider dataInheritDocFromTrait2 - * @param string $description - * @param string $expression */ public function testInheritDocFromTrait2( string $description, - string $expression + string $expression, ): void { require_once __DIR__ . '/data/inheritdoc-from-trait2-definition.php'; @@ -7476,18 +6770,16 @@ public function testInheritDocFromTrait2( $this->assertTypes( __DIR__ . '/data/inheritdoc-from-trait2.php', $description, - $expression + $expression, ); } /** * @dataProvider dataInheritDocFromTrait2 - * @param string $description - * @param string $expression */ public function testInheritDocWithoutCurlyBracesFromTrait2( string $description, - string $expression + string $expression, ): void { require_once __DIR__ . '/data/inheritdoc-without-curly-braces-from-trait2-definition.php'; @@ -7495,7 +6787,7 @@ public function testInheritDocWithoutCurlyBracesFromTrait2( $this->assertTypes( __DIR__ . '/data/inheritdoc-without-curly-braces-from-trait2.php', $description, - $expression + $expression, ); } @@ -7511,7 +6803,7 @@ public function dataResolveStatic(): array '\ResolveStatic\Bar::create()', ], [ - 'array(\'foo\' => ResolveStatic\Bar)', + 'array{foo: ResolveStatic\\Bar}', '$bar->returnConstantArray()', ], [ @@ -7527,18 +6819,16 @@ public function dataResolveStatic(): array /** * @dataProvider dataResolveStatic - * @param string $description - * @param string $expression */ public function testResolveStatic( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/resolve-static.php', $description, - $expression + $expression, ); } @@ -7561,12 +6851,7 @@ public function dataLoopVariables(): array "'end'", ], [ - 'LoopVariables\Bar|LoopVariables\Foo|LoopVariables\Lorem|null', - '$foo', - "'afterLoop'", - ], - [ - 'int|null', + 'int<1, max>|null', '$nullableVal', "'begin'", ], @@ -7576,15 +6861,10 @@ public function dataLoopVariables(): array "'nullableValIf'", ], [ - 'int', + 'int<10, max>', '$nullableVal', "'nullableValElse'", ], - [ - 'int|null', - '$nullableVal', - "'afterLoop'", - ], [ 'LoopVariables\Foo|false', '$falseOrObject', @@ -7595,11 +6875,6 @@ public function dataLoopVariables(): array '$falseOrObject', "'end'", ], - [ - 'LoopVariables\Foo|false', - '$falseOrObject', - "'afterLoop'", - ], ]; } @@ -7642,7 +6917,7 @@ public function dataForeachLoopVariables(): array "'end'", ], [ - 'array&nonEmpty', + 'non-empty-array', '$integers', "'end'", ], @@ -7657,7 +6932,7 @@ public function dataForeachLoopVariables(): array "'begin'", ], [ - 'array&nonEmpty', + 'non-empty-array', '$this->property', "'end'", ], @@ -7667,20 +6942,35 @@ public function dataForeachLoopVariables(): array "'afterLoop'", ], [ - 'int', + 'int<0, max>', '$i', "'begin'", ], [ - 'int', + 'int<0, max>', '$i', "'end'", ], [ - 'int', + 'int<0, max>', '$i', "'afterLoop'", ], + [ + 'LoopVariables\Bar|LoopVariables\Foo|LoopVariables\Lorem|null', + '$foo', + "'afterLoop'", + ], + [ + '1|int<10, max>|null', + '$nullableVal', + "'afterLoop'", + ], + [ + 'LoopVariables\Foo|false', + '$falseOrObject', + "'afterLoop'", + ], ]; } @@ -7688,20 +6978,35 @@ public function dataWhileLoopVariables(): array { return [ [ - 'int', + 'int<1, 10>', '$i', "'begin'", ], [ - 'int', + 'int<1, 10>', '$i', "'end'", ], [ - 'int', + 'int<0, 10>', '$i', "'afterLoop'", ], + [ + 'LoopVariables\Bar|LoopVariables\Foo|LoopVariables\Lorem|null', + '$foo', + "'afterLoop'", + ], + [ + '1|int<10, max>|null', + '$nullableVal', + "'afterLoop'", + ], + [ + 'LoopVariables\Foo|false', + '$falseOrObject', + "'afterLoop'", + ], ]; } @@ -7710,20 +7015,35 @@ public function dataForLoopVariables(): array { return [ [ - 'int', + 'int<0, 9>', '$i', "'begin'", ], [ - 'int', + 'int<0, 9>', '$i', "'end'", ], [ - 'int', + 'int<0, max>', '$i', "'afterLoop'", ], + [ + 'LoopVariables\Bar|LoopVariables\Foo|LoopVariables\Lorem', + '$foo', + "'afterLoop'", + ], + [ + '1|int<10, max>', + '$nullableVal', + "'afterLoop'", + ], + [ + 'LoopVariables\Foo', + '$falseOrObject', + "'afterLoop'", + ], ]; } @@ -7732,75 +7052,54 @@ public function dataForLoopVariables(): array /** * @dataProvider dataLoopVariables * @dataProvider dataForeachLoopVariables - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression */ public function testForeachLoopVariables( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( __DIR__ . '/data/foreach-loop-variables.php', $description, $expression, - [], - [], - [], - [], - $evaluatedPointExpression + $evaluatedPointExpression, ); } /** * @dataProvider dataLoopVariables * @dataProvider dataWhileLoopVariables - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression */ public function testWhileLoopVariables( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( __DIR__ . '/data/while-loop-variables.php', $description, $expression, - [], - [], - [], - [], - $evaluatedPointExpression + $evaluatedPointExpression, ); } /** * @dataProvider dataLoopVariables * @dataProvider dataForLoopVariables - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression */ public function testForLoopVariables( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( __DIR__ . '/data/for-loop-variables.php', $description, $expression, - [], - [], - [], - [], - $evaluatedPointExpression + $evaluatedPointExpression, ); } @@ -7828,22 +7127,22 @@ public function dataDoWhileLoopVariables(): array "'afterLoop'", ], [ - 'int', + 'int<0, max>', '$i', "'begin'", ], [ - 'int', + 'int<1, max>', '$i', "'end'", ], [ - 'int', + 'int<0, max>', '$i', "'afterLoop'", ], [ - 'int|null', + 'int<1, max>|null', '$nullableVal', "'begin'", ], @@ -7853,12 +7152,12 @@ public function dataDoWhileLoopVariables(): array "'nullableValIf'", ], [ - 'int', + 'int<10, max>', '$nullableVal', "'nullableValElse'", ], [ - 'int', + '1|int<10, max>', '$nullableVal', "'afterLoop'", ], @@ -7898,25 +7197,18 @@ public function dataDoWhileLoopVariables(): array /** * @dataProvider dataDoWhileLoopVariables - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression */ public function testDoWhileLoopVariables( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( __DIR__ . '/data/do-while-loop-variables.php', $description, $expression, - [], - [], - [], - [], - $evaluatedPointExpression + $evaluatedPointExpression, ); } @@ -7938,25 +7230,18 @@ public function dataMultipleClassesInOneFile(): array /** * @dataProvider dataMultipleClassesInOneFile - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression */ public function testMultipleClassesInOneFile( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( __DIR__ . '/data/multiple-classes-per-file.php', $description, $expression, - [], - [], - [], - [], - $evaluatedPointExpression + $evaluatedPointExpression, ); } @@ -7976,18 +7261,16 @@ public function dataCallingMultipleClassesInOneFile(): array /** * @dataProvider dataCallingMultipleClassesInOneFile - * @param string $description - * @param string $expression */ public function testCallingMultipleClassesInOneFile( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/calling-multiple-classes-per-file.php', $description, - $expression + $expression, ); } @@ -7995,7 +7278,7 @@ public function dataExplode(): array { return [ [ - 'array&nonEmpty', + 'non-empty-array', '$sureArray', ], [ @@ -8003,15 +7286,15 @@ public function dataExplode(): array '$sureFalse', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', '$arrayOrFalse', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', '$anotherArrayOrFalse', ], [ - PHP_VERSION_ID < 80000 ? '(array|false)' : 'array', + PHP_VERSION_ID < 80000 ? '(non-empty-array|false)' : 'non-empty-array', '$benevolentArrayOrFalse', ], ]; @@ -8019,18 +7302,16 @@ public function dataExplode(): array /** * @dataProvider dataExplode - * @param string $description - * @param string $expression */ public function testExplode( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/explode.php', $description, - $expression + $expression, ); } @@ -8090,18 +7371,16 @@ public function dataArrayPointerFunctions(): array /** * @dataProvider dataArrayPointerFunctions - * @param string $description - * @param string $expression */ public function testArrayPointerFunctions( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/array-pointer-functions.php', $description, - $expression + $expression, ); } @@ -8109,7 +7388,7 @@ public function dataReplaceFunctions(): array { return [ [ - 'string', + 'non-empty-string', '$expectedString', ], [ @@ -8117,19 +7396,19 @@ public function dataReplaceFunctions(): array '$expectedString2', ], [ - 'string|null', + 'non-empty-string|null', '$anotherExpectedString', ], [ - 'array(\'a\' => string, \'b\' => string)', + 'array{a: string, b: string}', '$expectedArray', ], [ - 'array(\'a\' => string, \'b\' => string)|null', + 'array{a: string, b: string}|null', '$expectedArray2', ], [ - 'array(\'a\' => string, \'b\' => string)|null', + 'array{a: string, b: string}|null', '$anotherExpectedArray', ], [ @@ -8149,7 +7428,7 @@ public function dataReplaceFunctions(): array '$anotherExpectedArrayOrString', ], [ - 'array(\'a\' => string, \'b\' => string)|null', + 'array{a: string, b: string}|null', 'preg_replace_callback_array($callbacks, $array)', ], [ @@ -8169,18 +7448,16 @@ public function dataReplaceFunctions(): array /** * @dataProvider dataReplaceFunctions - * @param string $description - * @param string $expression */ public function testReplaceFunctions( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/replaceFunctions.php', $description, - $expression + $expression, ); } @@ -8317,7 +7594,7 @@ public function dataFilterVarUnchanged(): array 'filter_var(3.27, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE)', ], [ - 'int', + 'int<0, max>', 'filter_var(rand(), FILTER_VALIDATE_INT)', ], [ @@ -8330,18 +7607,16 @@ public function dataFilterVarUnchanged(): array /** * @dataProvider dataFilterVar * @dataProvider dataFilterVarUnchanged - * @param string $description - * @param string $expression */ public function testFilterVar( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/filterVar.php', $description, - $expression + $expression, ); } @@ -8404,17 +7679,17 @@ public function dataClosureWithUsePassedByReference(): array "'beforeCallback'", ], [ - 'int', + 'int<1, max>', '$incrementedInside', "'inCallbackBeforeAssign'", ], [ - 'int', + 'int<2, max>', '$incrementedInside', "'inCallbackAfterAssign'", ], [ - 'int', + 'int<1, max>', '$incrementedInside', "'afterCallback'", ], @@ -8443,25 +7718,18 @@ public function dataClosureWithUsePassedByReference(): array /** * @dataProvider dataClosureWithUsePassedByReference - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression */ public function testClosureWithUsePassedByReference( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( __DIR__ . '/data/closure-passed-by-reference.php', $description, $expression, - [], - [], - [], - [], - $evaluatedPointExpression + $evaluatedPointExpression, ); } @@ -8477,18 +7745,16 @@ public function dataClosureWithUsePassedByReferenceInMethodCall(): array /** * @dataProvider dataClosureWithUsePassedByReferenceInMethodCall - * @param string $description - * @param string $expression */ public function testClosureWithUsePassedByReferenceInMethodCall( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/closure-passed-by-reference-in-call.php', $description, - $expression + $expression, ); } @@ -8530,42 +7796,33 @@ public function dataStaticClosure(): array /** * @dataProvider dataStaticClosure - * @param string $description - * @param string $expression */ public function testStaticClosure( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/static-closure.php', $description, - $expression + $expression, ); } /** * @dataProvider dataClosureWithUsePassedByReferenceReturn - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression */ public function testClosureWithUsePassedByReferenceReturn( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( __DIR__ . '/data/closure-passed-by-reference-return.php', $description, $expression, - [], - [], - [], - [], - $evaluatedPointExpression + $evaluatedPointExpression, ); } @@ -8585,25 +7842,19 @@ public function dataClosureWithInferredTypehint(): array /** * @dataProvider dataClosureWithInferredTypehint - * @param string $description - * @param string $expression */ public function testClosureWithInferredTypehint( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/closure-inferred-typehint.php', $description, $expression, - [], - [], - [], - [], 'die', [], - false + false, ); } @@ -8699,18 +7950,16 @@ public function dataTraitsPhpDocs(): array /** * @dataProvider dataTraitsPhpDocs - * @param string $description - * @param string $expression */ public function testTraitsPhpDocs( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/traits/traits.php', $description, - $expression + $expression, ); } @@ -8718,7 +7967,7 @@ public function dataPassedByReference(): array { return [ [ - 'array(1, 2, 3)', + 'array{1, 2, 3}', '$arr', ], [ @@ -8734,18 +7983,16 @@ public function dataPassedByReference(): array /** * @dataProvider dataPassedByReference - * @param string $description - * @param string $expression */ public function testPassedByReference( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/passed-by-reference.php', $description, - $expression + $expression, ); } @@ -8781,18 +8028,16 @@ public function dataCallables(): array /** * @dataProvider dataCallables - * @param string $description - * @param string $expression */ public function testCallables( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/callables.php', $description, - $expression + $expression, ); } @@ -8800,7 +8045,7 @@ public function dataArrayKeysInBranches(): array { return [ [ - 'array(\'i\' => int, \'j\' => int, \'k\' => int, \'key\' => DateTimeImmutable, \'l\' => 1, \'m\' => 5, ?\'n\' => \'str\')', + 'array{i: int<1, max>, j: int, k: int<1, max>, key: DateTimeImmutable, l: 1, m: 5, n?: \'str\'}', '$array', ], [ @@ -8812,7 +8057,7 @@ public function dataArrayKeysInBranches(): array '$generalArray[\'key\']', ], [ - 'array(0 => \'foo\', 1 => \'bar\', ?2 => \'baz\')', + 'array{0: \'foo\', 1: \'bar\', 2?: \'baz\'}', '$arrayAppendedInIf', ], [ @@ -8820,7 +8065,7 @@ public function dataArrayKeysInBranches(): array '$arrayAppendedInForeach', ], [ - 'array', + 'array, literal-string&non-empty-string>', // could be 'array, \'bar\'|\'baz\'|\'foo\'>' '$anotherArrayAppendedInForeach', ], [ @@ -8828,7 +8073,7 @@ public function dataArrayKeysInBranches(): array '$array[\'n\']', ], [ - 'int', + 'int<0, max>', '$incremented', ], [ @@ -8840,18 +8085,16 @@ public function dataArrayKeysInBranches(): array /** * @dataProvider dataArrayKeysInBranches - * @param string $description - * @param string $expression */ public function testArrayKeysInBranches( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/array-keys-branches.php', $description, - $expression + $expression, ); } @@ -8888,25 +8131,18 @@ public function dataSpecifiedFunctionCall(): array /** * @dataProvider dataSpecifiedFunctionCall - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression */ public function testSpecifiedFunctionCall( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( __DIR__ . '/data/specified-function-call.php', $description, $expression, - [], - [], - [], - [], - $evaluatedPointExpression + $evaluatedPointExpression, ); } @@ -8938,18 +8174,16 @@ public function dataElementsOnMixed(): array /** * @dataProvider dataElementsOnMixed - * @param string $description - * @param string $expression */ public function testElementsOnMixed( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/mixed-elements.php', $description, - $expression + $expression, ); } @@ -8969,18 +8203,16 @@ public function dataCaseInsensitivePhpDocTypes(): array /** * @dataProvider dataCaseInsensitivePhpDocTypes - * @param string $description - * @param string $expression */ public function testCaseInsensitivePhpDocTypes( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/case-insensitive-phpdocs.php', $description, - $expression + $expression, ); } @@ -9052,25 +8284,18 @@ public function dataConstantTypeAfterDuplicateCondition(): array /** * @dataProvider dataConstantTypeAfterDuplicateCondition - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression */ public function testConstantTypeAfterDuplicateCondition( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( __DIR__ . '/data/constant-types-duplicate-condition.php', $description, $expression, - [], - [], - [], - [], - $evaluatedPointExpression + $evaluatedPointExpression, ); } @@ -9112,25 +8337,18 @@ public function dataAnonymousClass(): array /** * @dataProvider dataAnonymousClass - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression */ public function testAnonymousClassName( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( __DIR__ . '/data/anonymous-class-name.php', $description, $expression, - [], - [], - [], - [], - $evaluatedPointExpression + $evaluatedPointExpression, ); } @@ -9146,18 +8364,16 @@ public function dataAnonymousClassInTrait(): array /** * @dataProvider dataAnonymousClassInTrait - * @param string $description - * @param string $expression */ public function testAnonymousClassNameInTrait( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/anonymous-class-name-in-trait.php', $description, - $expression + $expression, ); } @@ -9189,27 +8405,21 @@ public function dataDynamicConstants(): array /** * @dataProvider dataDynamicConstants - * @param string $description - * @param string $expression */ public function testDynamicConstants( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/dynamic-constant.php', $description, $expression, - [], - [], - [], - [], 'die', [ 'DynamicConstants\\DynamicConstantClass::DYNAMIC_CONSTANT_IN_CLASS', 'GLOBAL_DYNAMIC_CONSTANT', - ] + ], ); } @@ -9221,19 +8431,19 @@ public function dataIsset(): array '$array[\'b\']', ], [ - 'array(\'a\' => 1|2|3, \'b\' => 2|3, ?\'c\' => 4)', + 'array{a: 1|2|3, b: 2|3, c?: 4}', '$array', ], [ - 'array(\'a\' => 1|2|3, \'b\' => 2|3|null, ?\'c\' => 4)', + 'array{a: 1|2|3, b: 2|3|null, c?: 4}', '$arrayCopy', ], [ - 'array(\'a\' => 1|2|3, ?\'c\' => 4)', + 'array{a: 1|2|3, c?: 4}', '$anotherArrayCopy', ], [ - 'array', + 'array', '$yetAnotherArrayCopy', ], [ @@ -9281,18 +8491,16 @@ public function dataIsset(): array /** * @dataProvider dataIsset - * @param string $description - * @param string $expression */ public function testIsset( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/isset.php', $description, - $expression + $expression, ); } @@ -9305,7 +8513,7 @@ public function dataPropertyArrayAssignment(): array "'start'", ], [ - 'array()', + 'array{}', '$this->property', "'emptyArray'", ], @@ -9315,7 +8523,7 @@ public function dataPropertyArrayAssignment(): array "'emptyArray'", ], [ - 'array(\'foo\' => 1)', + 'array{foo: 1}', '$this->property', "'afterAssignment'", ], @@ -9329,25 +8537,18 @@ public function dataPropertyArrayAssignment(): array /** * @dataProvider dataPropertyArrayAssignment - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression */ public function testPropertyArrayAssignment( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( __DIR__ . '/data/property-array.php', $description, $expression, - [], - [], - [], - [], - $evaluatedPointExpression + $evaluatedPointExpression, ); } @@ -9375,18 +8576,16 @@ public function dataInArray(): array /** * @dataProvider dataInArray - * @param string $description - * @param string $expression */ public function testInArray( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/in-array.php', $description, - $expression + $expression, ); } @@ -9466,25 +8665,18 @@ public function dataGetParentClass(): array /** * @dataProvider dataGetParentClass - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression */ public function testGetParentClass( string $description, string $expression, - string $evaluatedPointExpression = 'die' + string $evaluatedPointExpression = 'die', ): void { $this->assertTypes( __DIR__ . '/data/get-parent-class.php', $description, $expression, - [], - [], - [], - [], - $evaluatedPointExpression + $evaluatedPointExpression, ); } @@ -9506,25 +8698,18 @@ public function dataIsCountable(): array /** * @dataProvider dataIsCountable - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression */ public function testIsCountable( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { $this->assertTypes( __DIR__ . '/data/is_countable.php', $description, $expression, - [], - [], - [], - [], - $evaluatedPointExpression + $evaluatedPointExpression, ); } @@ -9608,11 +8793,11 @@ public function dataPhp73Functions(): array 'array_key_last($anotherLiteralArray)', ], [ - 'array(int, int)', + 'array{int, int}', '$hrtime1', ], [ - 'array(int, int)', + 'array{int, int}', '$hrtime2', ], [ @@ -9620,7 +8805,7 @@ public function dataPhp73Functions(): array '$hrtime3', ], [ - 'array(int, int)|float|int', + 'array{int, int}|float|int', '$hrtime4', ], ]; @@ -9628,12 +8813,10 @@ public function dataPhp73Functions(): array /** * @dataProvider dataPhp73Functions - * @param string $description - * @param string $expression */ public function testPhp73Functions( string $description, - string $expression + string $expression, ): void { if (PHP_VERSION_ID < 70300) { @@ -9642,7 +8825,7 @@ public function testPhp73Functions( $this->assertTypes( __DIR__ . '/data/php73_functions.php', $description, - $expression + $expression, ); } @@ -9650,23 +8833,23 @@ public function dataPhp74Functions(): array { return [ [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', '$mbStrSplitConstantStringWithoutDefinedParameters', ], [ - "array('a', 'b', 'c', 'd', 'e', 'f')", + 'array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', '$mbStrSplitConstantStringWithoutDefinedSplitLength', ], [ - 'array', + 'non-empty-array', '$mbStrSplitStringWithoutDefinedSplitLength', ], [ - "array('a', 'b', 'c', 'd', 'e', 'f')", + 'array{\'a\', \'b\', \'c\', \'d\', \'e\', \'f\'}', '$mbStrSplitConstantStringWithOneSplitLength', ], [ - "array('abcdef')", + 'array{\'abcdef\'}', '$mbStrSplitConstantStringWithGreaterSplitLengthThanStringLength', ], [ @@ -9674,19 +8857,19 @@ public function dataPhp74Functions(): array '$mbStrSplitConstantStringWithFailureSplitLength', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', '$mbStrSplitConstantStringWithInvalidSplitLengthType', ], [ - 'array', + 'non-empty-array', '$mbStrSplitConstantStringWithVariableStringAndConstantSplitLength', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', '$mbStrSplitConstantStringWithVariableStringAndVariableSplitLength', ], [ - "array('a', 'b', 'c', 'd', 'e', 'f')", + "array{'a', 'b', 'c', 'd', 'e', 'f'}", '$mbStrSplitConstantStringWithOneSplitLengthAndValidEncoding', ], [ @@ -9694,11 +8877,11 @@ public function dataPhp74Functions(): array '$mbStrSplitConstantStringWithOneSplitLengthAndInvalidEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', '$mbStrSplitConstantStringWithOneSplitLengthAndVariableEncoding', ], [ - "array('abcdef')", + "array{'abcdef'}", '$mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndValidEncoding', ], [ @@ -9706,7 +8889,7 @@ public function dataPhp74Functions(): array '$mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndInvalidEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', '$mbStrSplitConstantStringWithGreaterSplitLengthThanStringLengthAndVariableEncoding', ], [ @@ -9722,7 +8905,7 @@ public function dataPhp74Functions(): array '$mbStrSplitConstantStringWithFailureSplitLengthAndVariableEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', '$mbStrSplitConstantStringWithInvalidSplitLengthTypeAndValidEncoding', ], [ @@ -9730,11 +8913,11 @@ public function dataPhp74Functions(): array '$mbStrSplitConstantStringWithInvalidSplitLengthTypeAndInvalidEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', '$mbStrSplitConstantStringWithInvalidSplitLengthTypeAndVariableEncoding', ], [ - 'array', + 'non-empty-array', '$mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndValidEncoding', ], [ @@ -9742,11 +8925,11 @@ public function dataPhp74Functions(): array '$mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndInvalidEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', '$mbStrSplitConstantStringWithVariableStringAndConstantSplitLengthAndVariableEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', '$mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndValidEncoding', ], [ @@ -9754,7 +8937,7 @@ public function dataPhp74Functions(): array '$mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndInvalidEncoding', ], [ - PHP_VERSION_ID < 80000 ? 'array|false' : 'array', + PHP_VERSION_ID < 80000 ? 'non-empty-array|false' : 'non-empty-array', '$mbStrSplitConstantStringWithVariableStringAndVariableSplitLengthAndVariableEncoding', ], ]; @@ -9762,12 +8945,10 @@ public function dataPhp74Functions(): array /** * @dataProvider dataPhp74Functions - * @param string $description - * @param string $expression */ public function testPhp74Functions( string $description, - string $expression + string $expression, ): void { if (PHP_VERSION_ID < 70400) { @@ -9776,7 +8957,7 @@ public function testPhp74Functions( $this->assertTypes( __DIR__ . '/data/php74_functions.php', $description, - $expression + $expression, ); } @@ -9796,18 +8977,16 @@ public function dataUnionMethods(): array /** * @dataProvider dataUnionMethods - * @param string $description - * @param string $expression */ public function testUnionMethods( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/union-methods.php', $description, - $expression + $expression, ); } @@ -9827,18 +9006,16 @@ public function dataUnionProperties(): array /** * @dataProvider dataUnionProperties - * @param string $description - * @param string $expression */ public function testUnionProperties( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/union-properties.php', $description, - $expression + $expression, ); } @@ -9854,18 +9031,16 @@ public function dataAssignmentInCondition(): array /** * @dataProvider dataAssignmentInCondition - * @param string $description - * @param string $expression */ public function testAssignmentInCondition( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/assignment-in-condition.php', $description, - $expression + $expression, ); } @@ -9873,7 +9048,7 @@ public function dataGeneralizeScope(): array { return [ [ - "array int, 'loadCount' => int, 'removeCount' => int, 'saveCount' => int)>>", + 'array, loadCount: int<0, max>, removeCount: int<0, max>, saveCount: int<0, max>}>>', '$statistics', ], ]; @@ -9881,18 +9056,16 @@ public function dataGeneralizeScope(): array /** * @dataProvider dataGeneralizeScope - * @param string $description - * @param string $expression */ public function testGeneralizeScope( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/generalize-scope.php', $description, - $expression + $expression, ); } @@ -9900,7 +9073,7 @@ public function dataGeneralizeScopeRecursiveType(): array { return [ [ - 'array()|array(\'foo\' => array)', + 'array{}|array{foo: array}', '$data', ], ]; @@ -9908,18 +9081,16 @@ public function dataGeneralizeScopeRecursiveType(): array /** * @dataProvider dataGeneralizeScopeRecursiveType - * @param string $description - * @param string $expression */ public function testGeneralizeScopeRecursiveType( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/generalize-scope-recursive.php', $description, - $expression + $expression, ); } @@ -9927,15 +9098,15 @@ public function dataArrayShapesInPhpDoc(): array { return [ [ - 'array(0 => string, 1 => ArrayShapesInPhpDoc\Foo, \'foo\' => ArrayShapesInPhpDoc\Bar, 2 => ArrayShapesInPhpDoc\Baz)', + 'array{0: string, 1: ArrayShapesInPhpDoc\\Foo, foo: ArrayShapesInPhpDoc\\Bar, 2: ArrayShapesInPhpDoc\\Baz}', '$one', ], [ - 'array(0 => string, ?1 => ArrayShapesInPhpDoc\Foo, ?\'foo\' => ArrayShapesInPhpDoc\Bar)', + 'array{0: string, 1?: ArrayShapesInPhpDoc\\Foo, foo?: ArrayShapesInPhpDoc\\Bar}', '$two', ], [ - 'array(?0 => string, ?1 => ArrayShapesInPhpDoc\Foo, ?\'foo\' => ArrayShapesInPhpDoc\Bar)', + 'array{0?: string, 1?: ArrayShapesInPhpDoc\\Foo, foo?: ArrayShapesInPhpDoc\\Bar}', '$three', ], ]; @@ -9943,18 +9114,16 @@ public function dataArrayShapesInPhpDoc(): array /** * @dataProvider dataArrayShapesInPhpDoc - * @param string $description - * @param string $expression */ public function testArrayShapesInPhpDoc( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/array-shapes.php', $description, - $expression + $expression, ); } @@ -9998,18 +9167,16 @@ public function dataInferPrivatePropertyTypeFromConstructor(): array /** * @dataProvider dataInferPrivatePropertyTypeFromConstructor - * @param string $description - * @param string $expression */ public function testInferPrivatePropertyTypeFromConstructor( string $description, - string $expression + string $expression, ): void { $this->assertTypes( __DIR__ . '/data/infer-private-property-type-from-constructor.php', $description, - $expression + $expression, ); } @@ -10033,12 +9200,10 @@ public function dataPropertyNativeTypes(): array /** * @dataProvider dataPropertyNativeTypes - * @param string $description - * @param string $expression */ public function testPropertyNativeTypes( string $description, - string $expression + string $expression, ): void { if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { @@ -10047,7 +9212,7 @@ public function testPropertyNativeTypes( $this->assertTypes( __DIR__ . '/data/property-native-types.php', $description, - $expression + $expression, ); } @@ -10063,7 +9228,7 @@ public function dataArrowFunctions(): array '$x()', ], [ - 'array(\'a\' => 1, \'b\' => 2)', + 'array{a: 1, b: 2}', '$y()', ], ]; @@ -10071,12 +9236,10 @@ public function dataArrowFunctions(): array /** * @dataProvider dataArrowFunctions - * @param string $description - * @param string $expression */ public function testArrowFunctions( string $description, - string $expression + string $expression, ): void { if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { @@ -10085,7 +9248,7 @@ public function testArrowFunctions( $this->assertTypes( __DIR__ . '/data/arrow-functions.php', $description, - $expression + $expression, ); } @@ -10109,12 +9272,10 @@ public function dataArrowFunctionsInside(): array /** * @dataProvider dataArrowFunctionsInside - * @param string $description - * @param string $expression */ public function testArrowFunctionsInside( string $description, - string $expression + string $expression, ): void { if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { @@ -10123,7 +9284,7 @@ public function testArrowFunctionsInside( $this->assertTypes( __DIR__ . '/data/arrow-functions-inside.php', $description, - $expression + $expression, ); } @@ -10151,11 +9312,11 @@ public function dataCoalesceAssign(): array '$arrayWithMaybeFoo[\'foo\'] ??= \'bar\'', ], [ - 'array(\'foo\' => \'foo\')', + 'array{foo: \'foo\'}', '$arrayAfterAssignment', ], [ - 'array(\'foo\' => \'foo\')', + 'array{foo: \'foo\'}', '$arrayWithFooAfterAssignment', ], [ @@ -10171,12 +9332,10 @@ public function dataCoalesceAssign(): array /** * @dataProvider dataCoalesceAssign - * @param string $description - * @param string $expression */ public function testCoalesceAssign( string $description, - string $expression + string $expression, ): void { if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { @@ -10185,7 +9344,7 @@ public function testCoalesceAssign( $this->assertTypes( __DIR__ . '/data/coalesce-assign.php', $description, - $expression + $expression, ); } @@ -10193,31 +9352,31 @@ public function dataArraySpread(): array { return [ [ - 'array&nonEmpty', + 'non-empty-array', '$integersOne', ], [ - 'array&nonEmpty', + 'non-empty-array', '$integersTwo', ], [ - 'array(1, 2, 3, 4, 5, 6, 7)', + 'array{1, 2, 3, 4, 5, 6, 7}', '$integersThree', ], [ - 'array&nonEmpty', + 'non-empty-array', '$integersFour', ], [ - 'array&nonEmpty', + 'non-empty-array', '$integersFive', ], [ - 'array(1, 2, 3, 4, 5, 6, 7)', + 'array{1, 2, 3, 4, 5, 6, 7}', '$integersSix', ], [ - 'array(1, 2, 3, 4, 5, 6, 7)', + 'array{1, 2, 3, 4, 5, 6, 7}', '$integersSeven', ], ]; @@ -10225,12 +9384,10 @@ public function dataArraySpread(): array /** * @dataProvider dataArraySpread - * @param string $description - * @param string $expression */ public function testArraySpread( string $description, - string $expression + string $expression, ): void { if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { @@ -10239,7 +9396,7 @@ public function testArraySpread( $this->assertTypes( __DIR__ . '/data/array-spread.php', $description, - $expression + $expression, ); } @@ -10247,7 +9404,7 @@ public function dataPhp74FunctionsIn73(): array { return [ [ - '*ERROR*', + 'mixed', 'password_algos()', ], ]; @@ -10255,12 +9412,10 @@ public function dataPhp74FunctionsIn73(): array /** * @dataProvider dataPhp74FunctionsIn73 - * @param string $description - * @param string $expression */ public function testPhp74FunctionsIn73( string $description, - string $expression + string $expression, ): void { if (PHP_VERSION_ID >= 70400) { @@ -10269,7 +9424,7 @@ public function testPhp74FunctionsIn73( $this->assertTypes( __DIR__ . '/data/die-73.php', $description, - $expression + $expression, ); } @@ -10285,12 +9440,10 @@ public function dataPhp74FunctionsIn74(): array /** * @dataProvider dataPhp74FunctionsIn74 - * @param string $description - * @param string $expression */ public function testPhp74FunctionsIn74( string $description, - string $expression + string $expression, ): void { if (PHP_VERSION_ID < 70400) { @@ -10299,7 +9452,7 @@ public function testPhp74FunctionsIn74( $this->assertTypes( __DIR__ . '/data/die-74.php', $description, - $expression + $expression, ); } @@ -10326,78 +9479,43 @@ public function dataTryCatchScope(): array /** * @dataProvider dataTryCatchScope - * @param string $description - * @param string $expression - * @param string $evaluatedPointExpression */ public function testTryCatchScope( string $description, string $expression, - string $evaluatedPointExpression + string $evaluatedPointExpression, ): void { - foreach ([true, false] as $polluteCatchScopeWithTryAssignments) { - $this->polluteCatchScopeWithTryAssignments = $polluteCatchScopeWithTryAssignments; - - try { - $this->assertTypes( - __DIR__ . '/data/try-catch-scope.php', - $description, - $expression, - [], - [], - [], - [], - $evaluatedPointExpression, - [], - false - ); - } catch (\PHPUnit\Framework\ExpectationFailedException $e) { - throw new \PHPUnit\Framework\ExpectationFailedException( - sprintf( - '%s (polluteCatchScopeWithTryAssignments: %s)', - $e->getMessage(), - $polluteCatchScopeWithTryAssignments ? 'true' : 'false' - ), - $e->getComparisonFailure() - ); - } - } + $this->assertTypes( + __DIR__ . '/data/try-catch-scope.php', + $description, + $expression, + $evaluatedPointExpression, + [], + false, + ); } /** - * @param string $file - * @param string $description - * @param string $expression - * @param DynamicMethodReturnTypeExtension[] $dynamicMethodReturnTypeExtensions - * @param DynamicStaticMethodReturnTypeExtension[] $dynamicStaticMethodReturnTypeExtensions - * @param \PHPStan\Type\MethodTypeSpecifyingExtension[] $methodTypeSpecifyingExtensions - * @param \PHPStan\Type\StaticMethodTypeSpecifyingExtension[] $staticMethodTypeSpecifyingExtensions - * @param string $evaluatedPointExpression * @param string[] $dynamicConstantNames - * @param bool $useCache */ private function assertTypes( string $file, string $description, string $expression, - array $dynamicMethodReturnTypeExtensions = [], - array $dynamicStaticMethodReturnTypeExtensions = [], - array $methodTypeSpecifyingExtensions = [], - array $staticMethodTypeSpecifyingExtensions = [], string $evaluatedPointExpression = 'die', array $dynamicConstantNames = [], - bool $useCache = true + bool $useCache = true, ): void { $assertType = function (Scope $scope) use ($expression, $description, $evaluatedPointExpression): void { - /** @var \PhpParser\Node\Stmt\Expression $expressionNode */ + /** @var Node\Stmt\Expression $expressionNode */ $expressionNode = $this->getParser()->parseString(sprintf('getType($expressionNode->expr); $this->assertTypeDescribe( $description, $type, - sprintf('%s at %s', $expression, $evaluatedPointExpression) + sprintf('%s at %s', $expression, $evaluatedPointExpression), ); }; if ($useCache && isset(self::$assertTypesCache[$file][$evaluatedPointExpression])) { @@ -10406,11 +9524,11 @@ private function assertTypes( } $this->processFile( $file, - static function (\PhpParser\Node $node, Scope $scope) use ($file, $evaluatedPointExpression, $assertType): void { + static function (Node $node, Scope $scope) use ($file, $evaluatedPointExpression, $assertType): void { if ($node instanceof VirtualNode) { return; } - $printer = new \PhpParser\PrettyPrinter\Standard(); + $printer = new Standard(); $printedNode = $printer->prettyPrint([$node]); if ($printedNode !== $evaluatedPointExpression) { return; @@ -10420,11 +9538,7 @@ static function (\PhpParser\Node $node, Scope $scope) use ($file, $evaluatedPoin $assertType($scope); }, - $dynamicMethodReturnTypeExtensions, - $dynamicStaticMethodReturnTypeExtensions, - $methodTypeSpecifyingExtensions, - $staticMethodTypeSpecifyingExtensions, - $dynamicConstantNames + $dynamicConstantNames, ); } @@ -10456,12 +9570,10 @@ public function dataDeclareStrictTypes(): array /** * @dataProvider dataDeclareStrictTypes - * @param string $file - * @param bool $result */ public function testDeclareStrictTypes(string $file, bool $result): void { - $this->processFile($file, function (\PhpParser\Node $node, Scope $scope) use ($result): void { + $this->processFile($file, function (Node $node, Scope $scope) use ($result): void { if (!($node instanceof Exit_)) { return; } @@ -10472,7 +9584,7 @@ public function testDeclareStrictTypes(string $file, bool $result): void public function testEarlyTermination(): void { - $this->processFile(__DIR__ . '/data/early-termination.php', function (\PhpParser\Node $node, Scope $scope): void { + $this->processFile(__DIR__ . '/data/early-termination.php', function (Node $node, Scope $scope): void { if (!($node instanceof Exit_)) { return; } @@ -10501,14 +9613,14 @@ protected function getEarlyTerminatingFunctionCalls(): array private function assertTypeDescribe( string $expectedDescription, Type $actualType, - string $label = '' + string $label = '', ): void { $actualDescription = $actualType->describe(VerbosityLevel::precise()); $this->assertSame( $expectedDescription, $actualDescription, - $label + $label, ); } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 5bfb578ccf..09472ed1b1 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -3,12 +3,20 @@ namespace PHPStan\Analyser; use PHPStan\Testing\TypeInferenceTestCase; +use stdClass; +use function define; +use function extension_loaded; +use const PHP_INT_SIZE; +use const PHP_VERSION_ID; class NodeScopeResolverTest extends TypeInferenceTestCase { public function dataFileAsserts(): iterable { + require_once __DIR__ . '/data/implode.php'; + yield from $this->gatherAssertTypes(__DIR__ . '/data/implode.php'); + require_once __DIR__ . '/data/bug2574.php'; yield from $this->gatherAssertTypes(__DIR__ . '/data/bug2574.php'); @@ -25,11 +33,19 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-class-string.php'); + require_once __DIR__ . '/data/generic-generalization.php'; + + yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-generalization.php'); + require_once __DIR__ . '/data/instanceof.php'; + yield from $this->gatherAssertTypes(__DIR__ . '/data/date.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/instanceof.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/integer-range-types.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/random-int.php'); + if (PHP_INT_SIZE === 8) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/random-int.php'); + } + 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/intersection-static.php'); @@ -54,6 +70,8 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/native-types.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/type-change-after-array-access-assignment.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/iterator_to_array.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/key-of.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/value-of.php'); if (self::$useStaticReflectionProvider || extension_loaded('ds')) { yield from $this->gatherAssertTypes(__DIR__ . '/data/ext-ds.php'); @@ -63,6 +81,7 @@ public function dataFileAsserts(): iterable } yield from $this->gatherAssertTypes(__DIR__ . '/data/is-numeric.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/is-a.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/is-subclass-of.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3142.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/array-shapes-keys-strings.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1216.php'); @@ -93,7 +112,11 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/catch-without-variable.php'); } yield from $this->gatherAssertTypes(__DIR__ . '/data/mixed-typehint.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2600.php'); + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2600-php8.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2600.php'); + } yield from $this->gatherAssertTypes(__DIR__ . '/data/array-typehint-without-null-in-phpdoc.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/override-root-scope-variable.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bitwise-not.php'); @@ -264,8 +287,19 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/infer-array-key.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/offset-value-after-assign.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2112.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-filter.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-filter-callables.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-filter-string-callables.php'); + if (PHP_VERSION_ID >= 70400) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-filter-arrow-functions.php'); + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-flip.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-map.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/array-map-closure.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/array-sum.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-plus.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4573.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4577.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4579.php'); @@ -425,24 +459,349 @@ public function dataFileAsserts(): iterable if (self::$useStaticReflectionProvider || PHP_VERSION_ID >= 70400) { yield from $this->gatherAssertTypes(__DIR__ . '/data/arrow-function-types.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4902.php'); + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4902-php8.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4902.php'); + } } yield from $this->gatherAssertTypes(__DIR__ . '/data/closure-types.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5219.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/strval.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-next.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-string.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-string-replace-functions.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3981.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4711.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/sscanf.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-offset-get.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-object-lower-bound.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/class-reflection-interfaces.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-4415.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5259.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5293.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5129.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4970.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5322.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/splfixedarray-iterator-types.php'); + + if (PHP_VERSION_ID >= 70400 || self::$useStaticReflectionProvider) { + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-5372.php'); + } + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/bug-5372_2.php'); + + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/mb_substitute_character-php8.php'); + } elseif (PHP_VERSION_ID < 70200) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/mb_substitute_character-php71.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/mb_substitute_character.php'); + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/class-constant-types.php'); + + if (self::$useStaticReflectionProvider) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3379.php'); + } + + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/reflectionclass-issue-5511-php8.php'); + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/modulo-operator.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/literal-string.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/filter-var-returns-non-empty-string.php'); + + if (PHP_VERSION_ID >= 80000 || self::$useStaticReflectionProvider) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/model-mixin.php'); + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5529.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/sizeof.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/div-by-zero.php'); + + if (PHP_INT_SIZE === 8) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5072.php'); + } + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5530.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1861.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4843.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4602.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4499.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2142.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5584.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/math.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1870.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-5562.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5615.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/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'); + + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/variadic-parameter-php8.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4896.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5843.php'); + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/eval-implicit-throw.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5628.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5501.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4743.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5017.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5992.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6001.php'); + + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/round-php8.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/round.php'); + } + + if (PHP_VERSION_ID >= 80100) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5287-php81.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5287.php'); + } + + if (PHP_VERSION_ID >= 70400) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5458.php'); + } + + if (PHP_VERSION_ID >= 80100 || self::$useStaticReflectionProvider) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/never.php'); + } + + if (PHP_VERSION_ID >= 80100 || self::$useStaticReflectionProvider) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/native-intersection.php'); + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2760.php'); + + if (PHP_VERSION_ID >= 80100 || self::$useStaticReflectionProvider) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/new-in-initializers.php'); + + if (PHP_VERSION_ID >= 80100) { + define('TEST_OBJECT_CONSTANT', new stdClass()); + yield from $this->gatherAssertTypes(__DIR__ . '/data/new-in-initializers-runtime.php'); + } + } + + if (PHP_VERSION_ID >= 80100 || self::$useStaticReflectionProvider) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/first-class-callables.php'); + } + + if (PHP_VERSION_ID >= 80100) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-is-list-type-specifying.php'); + } + + if (PHP_VERSION_ID >= 80100) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-unpacking-string-keys.php'); + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/filesystem-functions.php'); + + if (PHP_VERSION_ID >= 80100) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/enums.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/enums-import-alias.php'); + } + + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6293.php'); + } + + if (PHP_VERSION_ID >= 70200) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/predefined-constants-php72.php'); + } + if (PHP_VERSION_ID >= 70400) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/predefined-constants-php74.php'); + } + if (PHP_INT_SIZE === 8) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/predefined-constants-64bit.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/predefined-constants-32bit.php'); + } + yield from $this->gatherAssertTypes(__DIR__ . '/data/predefined-constants.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/classPhpDocs-phpstanPropertyPrefix.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-destructuring-types.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/pdo-prepare.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/constant-array-type-set.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/for-loop-i-type.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5316.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3858.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2806.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5328.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3044.php'); + + if (PHP_VERSION_ID >= 80100) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/invalidate-readonly-properties.php'); + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/weird-array_key_exists-issue.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/equal.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/identical.php'); + + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5698-php8.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5698-php7.php'); + } + + if (PHP_VERSION_ID >= 70304) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/date-period-return-types.php'); + } + + 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-5817.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-column.php'); + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-column-php8.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-column-php7.php'); + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6497.php'); + + if (PHP_VERSION_ID >= 70400) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/isset-coalesce-empty-type.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/isset-coalesce-empty-type-root.php'); + } + + if (PHP_VERSION_ID < 80100) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/isset-coalesce-empty-type-pre-81.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/isset-coalesce-empty-type-post-81.php'); + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/template-null-bound.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4592.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4903.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2420.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-2718.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3126.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4586.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4887.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/hash-functions.php'); + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/hash-functions-80.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/hash-functions-74.php'); + } + + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6308.php'); + } + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6329.php'); + + if (PHP_VERSION_ID >= 70400) { + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-6473.php'); + } + + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6566-types.php'); + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6500.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6488.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6624.php'); + + if (PHP_VERSION_ID >= 70400) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/property-template-tag.php'); + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6672.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6687.php'); + + if (self::$useStaticReflectionProvider) { + 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'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/preg_match_php8.php'); + } + + require_once __DIR__ . '/data/countable.php'; + yield from $this->gatherAssertTypes(__DIR__ . '/data/countable.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6696.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6704.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/filter-iterator-child-class.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/smaller-than-benevolent.php'); + + if (PHP_VERSION_ID >= 80100) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6695.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6433.php'); + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6698.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/date-format.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6070.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6108.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1516.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6174.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-5749.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5675.php'); + + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6505.php'); + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6305.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6699.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6715.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6682.php'); + + yield from $this->gatherAssertTypes(__DIR__ . '/data/preg_filter.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5759.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5668.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/generics-empty-array.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-5757.php'); + + if (PHP_VERSION_ID >= 70400) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/nullable-closure-parameter.php'); + } + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-6635.php'); + } + + if (PHP_VERSION_ID >= 70400) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6591.php'); + } + + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6790.php'); + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6584.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6439.php'); } /** * @dataProvider dataFileAsserts - * @param string $assertType - * @param string $file * @param mixed ...$args */ public function testFileAsserts( string $assertType, string $file, - ...$args + ...$args, ): void { $this->assertFileAsserts($assertType, $file, ...$args); diff --git a/tests/PHPStan/Analyser/ScopeTest.php b/tests/PHPStan/Analyser/ScopeTest.php index 4c1f9c663c..4f9e0a4ce4 100644 --- a/tests/PHPStan/Analyser/ScopeTest.php +++ b/tests/PHPStan/Analyser/ScopeTest.php @@ -4,17 +4,18 @@ use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Name\FullyQualified; -use PHPStan\Testing\TestCase; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; -class ScopeTest extends TestCase +class ScopeTest extends PHPStanTestCase { public function dataGeneralize(): array @@ -28,12 +29,12 @@ public function dataGeneralize(): array [ new ConstantStringType('a'), new ConstantStringType('b'), - 'string', + 'literal-string&non-empty-string', ], [ new ConstantIntegerType(0), new ConstantIntegerType(1), - 'int', + 'int<0, max>', ], [ new UnionType([ @@ -45,7 +46,7 @@ public function dataGeneralize(): array new ConstantIntegerType(1), new ConstantIntegerType(2), ]), - 'int', + 'int<0, max>', ], [ new UnionType([ @@ -72,7 +73,7 @@ public function dataGeneralize(): array new ConstantIntegerType(2), new ConstantStringType('foo'), ]), - '\'foo\'|int', + '\'foo\'|int<0, max>', ], [ new ConstantBooleanType(false), @@ -93,7 +94,7 @@ public function dataGeneralize(): array [ new ObjectType('Foo'), new ConstantBooleanType(false), - 'Foo', + 'Foo|false', ], [ new ConstantArrayType([ @@ -106,7 +107,7 @@ public function dataGeneralize(): array ], [ new ConstantIntegerType(1), ]), - 'array(\'a\' => 1)', + 'array{a: 1}', ], [ new ConstantArrayType([ @@ -123,7 +124,7 @@ public function dataGeneralize(): array new ConstantIntegerType(2), new ConstantIntegerType(1), ]), - 'array(\'a\' => int, \'b\' => 1)', + 'array{a: int<1, max>, b: 1}', ], [ new ConstantArrayType([ @@ -138,7 +139,7 @@ public function dataGeneralize(): array new ConstantIntegerType(1), new ConstantIntegerType(1), ]), - 'array', + 'array', ], [ new ConstantArrayType([ @@ -153,16 +154,79 @@ public function dataGeneralize(): array new ConstantIntegerType(1), new ConstantIntegerType(2), ]), - 'array', + 'array>', + ], + [ + new UnionType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ]), + new UnionType([ + new ConstantIntegerType(-1), + new ConstantIntegerType(0), + new ConstantIntegerType(1), + ]), + 'int', + ], + [ + new UnionType([ + new ConstantIntegerType(0), + new ConstantIntegerType(2), + ]), + new UnionType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + new ConstantIntegerType(2), + ]), + '0|1|2', + ], + [ + new UnionType([ + new ConstantIntegerType(0), + new ConstantIntegerType(1), + new ConstantIntegerType(2), + ]), + new UnionType([ + new ConstantIntegerType(0), + new ConstantIntegerType(2), + ]), + '0|1|2', + ], + [ + IntegerRangeType::fromInterval(0, 16), + IntegerRangeType::fromInterval(1, 17), + 'int<0, max>', + ], + [ + IntegerRangeType::fromInterval(0, 16), + IntegerRangeType::fromInterval(-1, 15), + 'int', + ], + [ + IntegerRangeType::fromInterval(0, 16), + IntegerRangeType::fromInterval(1, null), + 'int<0, max>', + ], + [ + IntegerRangeType::fromInterval(0, 16), + IntegerRangeType::fromInterval(null, 15), + 'int', + ], + [ + IntegerRangeType::fromInterval(0, 16), + IntegerRangeType::fromInterval(0, null), + 'int<0, max>', + ], + [ + IntegerRangeType::fromInterval(0, 16), + IntegerRangeType::fromInterval(null, 16), + 'int', ], ]; } /** * @dataProvider dataGeneralize - * @param Type $a - * @param Type $b - * @param string $expectedTypeDescription */ public function testGeneralize(Type $a, Type $b, string $expectedTypeDescription): void { @@ -181,7 +245,7 @@ public function testGetConstantType(): void $scope = $scopeFactory->create(ScopeContext::create(__DIR__ . '/data/compiler-halt-offset.php')); $node = new ConstFetch(new FullyQualified('__COMPILER_HALT_OFFSET__')); $type = $scope->getType($node); - $this->assertSame('int', $type->describe(VerbosityLevel::precise())); + $this->assertSame('int<0, max>', $type->describe(VerbosityLevel::precise())); } } diff --git a/tests/PHPStan/Analyser/StatementResultTest.php b/tests/PHPStan/Analyser/StatementResultTest.php index 966db5f6ab..228ad689c6 100644 --- a/tests/PHPStan/Analyser/StatementResultTest.php +++ b/tests/PHPStan/Analyser/StatementResultTest.php @@ -4,12 +4,14 @@ use PhpParser\Node\Stmt; use PHPStan\Parser\Parser; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\ArrayType; use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\StringType; +use function sprintf; -class StatementResultTest extends \PHPStan\Testing\TestCase +class StatementResultTest extends PHPStanTestCase { public function dataIsAlwaysTerminating(): array @@ -376,12 +378,10 @@ public function dataIsAlwaysTerminating(): array /** * @dataProvider dataIsAlwaysTerminating - * @param string $code - * @param bool $expectedIsAlwaysTerminating */ public function testIsAlwaysTerminating( string $code, - bool $expectedIsAlwaysTerminating + bool $expectedIsAlwaysTerminating, ): void { /** @var Parser $parser */ @@ -404,7 +404,7 @@ public function testIsAlwaysTerminating( $stmts, $scope, static function (): void { - } + }, ); $this->assertSame($expectedIsAlwaysTerminating, $result->isAlwaysTerminating()); } diff --git a/tests/PHPStan/Analyser/TypeSpecifierContextTest.php b/tests/PHPStan/Analyser/TypeSpecifierContextTest.php index ac18184fd0..c6e083ed5d 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierContextTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierContextTest.php @@ -2,7 +2,10 @@ namespace PHPStan\Analyser; -class TypeSpecifierContextTest extends \PHPStan\Testing\TestCase +use PHPStan\ShouldNotHappenException; +use PHPStan\Testing\PHPStanTestCase; + +class TypeSpecifierContextTest extends PHPStanTestCase { public function dataContext(): array @@ -33,7 +36,6 @@ public function dataContext(): array /** * @dataProvider dataContext - * @param \PHPStan\Analyser\TypeSpecifierContext $context * @param bool[] $results */ public function testContext(TypeSpecifierContext $context, array $results): void @@ -69,7 +71,6 @@ public function dataNegate(): array /** * @dataProvider dataNegate - * @param \PHPStan\Analyser\TypeSpecifierContext $context * @param bool[] $results */ public function testNegate(TypeSpecifierContext $context, array $results): void @@ -83,7 +84,7 @@ public function testNegate(TypeSpecifierContext $context, array $results): void public function testNegateNull(): void { - $this->expectException(\PHPStan\ShouldNotHappenException::class); + $this->expectException(ShouldNotHappenException::class); TypeSpecifierContext::createNull()->negate(); } diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index b6bdb05a90..43af6df355 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierTest.php @@ -8,6 +8,7 @@ use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\BinaryOp\NotIdentical; use PhpParser\Node\Expr\BooleanNot; +use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Expr\Variable; @@ -16,41 +17,48 @@ use PhpParser\Node\Scalar\LNumber; use PhpParser\Node\Scalar\String_; use PhpParser\Node\VarLikeIdentifier; +use PhpParser\PrettyPrinter\Standard; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\ArrayType; use PHPStan\Type\ClassStringType; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\FloatType; use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function implode; +use function sprintf; +use const PHP_INT_MAX; +use const PHP_INT_MIN; -class TypeSpecifierTest extends \PHPStan\Testing\TestCase +class TypeSpecifierTest extends PHPStanTestCase { - private const FALSEY_TYPE_DESCRIPTION = '0|0.0|\'\'|\'0\'|array()|false|null'; + private const FALSEY_TYPE_DESCRIPTION = '0|0.0|\'\'|\'0\'|array{}|false|null'; private const TRUTHY_TYPE_DESCRIPTION = 'mixed~' . self::FALSEY_TYPE_DESCRIPTION; private const SURE_NOT_FALSEY = '~' . self::FALSEY_TYPE_DESCRIPTION; private const SURE_NOT_TRUTHY = '~' . self::TRUTHY_TYPE_DESCRIPTION; - /** @var \PhpParser\PrettyPrinter\Standard() */ - private $printer; + /** @var Standard () */ + private Standard $printer; - /** @var \PHPStan\Analyser\TypeSpecifier */ - private $typeSpecifier; + private TypeSpecifier $typeSpecifier; - /** @var Scope */ - private $scope; + private Scope $scope; protected function setUp(): void { - $broker = $this->createBroker(); - $this->printer = new \PhpParser\PrettyPrinter\Standard(); - $this->typeSpecifier = $this->createTypeSpecifier($this->printer, $broker); - $this->scope = $this->createScopeFactory($broker, $this->typeSpecifier)->create(ScopeContext::create('')); - $this->scope = $this->scope->enterClass($broker->getClass('DateTime')); + $reflectionProvider = $this->createReflectionProvider(); + $this->printer = new Standard(); + $this->typeSpecifier = self::getContainer()->getService('typeSpecifier'); + $this->scope = $this->createScopeFactory($reflectionProvider, $this->typeSpecifier)->create(ScopeContext::create('')); + $this->scope = $this->scope->enterClass($reflectionProvider->getClass('DateTime')); $this->scope = $this->scope->assignVariable('bar', new ObjectType('Bar')); $this->scope = $this->scope->assignVariable('stringOrNull', new UnionType([new StringType(), new NullType()])); $this->scope = $this->scope->assignVariable('string', new StringType()); @@ -61,11 +69,13 @@ protected function setUp(): void $this->scope = $this->scope->assignVariable('foo', new MixedType()); $this->scope = $this->scope->assignVariable('classString', new ClassStringType()); $this->scope = $this->scope->assignVariable('genericClassString', new GenericClassStringType(new ObjectType('Bar'))); + $this->scope = $this->scope->assignVariable('object', new ObjectWithoutClassType()); + $this->scope = $this->scope->assignVariable('int', new IntegerType()); + $this->scope = $this->scope->assignVariable('float', new FloatType()); } /** * @dataProvider dataCondition - * @param Expr $expr * @param mixed[] $expectedPositiveResult * @param mixed[] $expectedNegatedResult */ @@ -90,7 +100,7 @@ public function dataCondition(): array ], [ $this->createFunctionCall('is_numeric'), - ['$foo' => 'float|int|(string&numeric)'], + ['$foo' => 'float|int|numeric-string'], ['$foo' => '~float|int'], ], [ @@ -101,7 +111,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\BooleanAnd( $this->createFunctionCall('is_int'), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), ['$foo' => 'int'], [], @@ -109,7 +119,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\BooleanOr( $this->createFunctionCall('is_int'), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), [], ['$foo' => '~int'], @@ -117,7 +127,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\LogicalAnd( $this->createFunctionCall('is_int'), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), ['$foo' => 'int'], [], @@ -125,7 +135,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\LogicalOr( $this->createFunctionCall('is_int'), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), [], ['$foo' => '~int'], @@ -139,7 +149,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\BooleanAnd( new Expr\BooleanNot($this->createFunctionCall('is_int')), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), ['$foo' => '~int'], [], @@ -147,7 +157,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\BooleanOr( new Expr\BooleanNot($this->createFunctionCall('is_int')), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), [], ['$foo' => 'int'], @@ -170,7 +180,7 @@ public function dataCondition(): array [ new Expr\Instanceof_( new Variable('foo'), - new Variable('className') + new Variable('className'), ), ['$foo' => 'object'], [], @@ -180,7 +190,7 @@ public function dataCondition(): array new FuncCall(new Name('get_class'), [ new Arg(new Variable('foo')), ]), - new String_('Foo') + new String_('Foo'), ), ['$foo' => 'Foo'], ['$foo' => '~Foo'], @@ -190,7 +200,7 @@ public function dataCondition(): array new String_('Foo'), new FuncCall(new Name('get_class'), [ new Arg(new Variable('foo')), - ]) + ]), ), ['$foo' => 'Foo'], ['$foo' => '~Foo'], @@ -199,8 +209,8 @@ public function dataCondition(): array new BooleanNot( new Expr\Instanceof_( new Variable('foo'), - new Variable('className') - ) + new Variable('className'), + ), ), [], ['$foo' => 'object'], @@ -213,7 +223,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\BooleanAnd( new Variable('foo'), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), ['$foo' => self::SURE_NOT_FALSEY], [], @@ -221,7 +231,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\BooleanOr( new Variable('foo'), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), [], ['$foo' => self::SURE_NOT_TRUTHY], @@ -240,7 +250,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\BooleanAnd( new PropertyFetch(new Variable('this'), 'foo'), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), ['$this->foo' => self::SURE_NOT_FALSEY], [], @@ -248,7 +258,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\BooleanOr( new PropertyFetch(new Variable('this'), 'foo'), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), [], ['$this->foo' => self::SURE_NOT_TRUTHY], @@ -262,7 +272,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\BooleanOr( $this->createFunctionCall('is_int'), - $this->createFunctionCall('is_string') + $this->createFunctionCall('is_string'), ), ['$foo' => 'int|string'], ['$foo' => '~int|string'], @@ -272,8 +282,8 @@ public function dataCondition(): array $this->createFunctionCall('is_int'), new Expr\BinaryOp\BooleanOr( $this->createFunctionCall('is_string'), - $this->createFunctionCall('is_bool') - ) + $this->createFunctionCall('is_bool'), + ), ), ['$foo' => 'bool|int|string'], ['$foo' => '~bool|int|string'], @@ -281,7 +291,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\BooleanOr( $this->createFunctionCall('is_int', 'foo'), - $this->createFunctionCall('is_string', 'bar') + $this->createFunctionCall('is_string', 'bar'), ), [], ['$foo' => '~int', '$bar' => '~string'], @@ -290,9 +300,9 @@ public function dataCondition(): array new Expr\BinaryOp\BooleanAnd( new Expr\BinaryOp\BooleanOr( $this->createFunctionCall('is_int', 'foo'), - $this->createFunctionCall('is_string', 'foo') + $this->createFunctionCall('is_string', 'foo'), ), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), ['$foo' => 'int|string'], [], @@ -301,20 +311,20 @@ public function dataCondition(): array new Expr\BinaryOp\BooleanOr( new Expr\BinaryOp\BooleanAnd( $this->createFunctionCall('is_int', 'foo'), - $this->createFunctionCall('is_string', 'foo') + $this->createFunctionCall('is_string', 'foo'), ), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), [], - ['$foo' => '~*NEVER*'], + ['$foo' => 'mixed'], ], [ new Expr\BinaryOp\BooleanOr( new Expr\BinaryOp\BooleanAnd( $this->createFunctionCall('is_int', 'foo'), - $this->createFunctionCall('is_string', 'bar') + $this->createFunctionCall('is_string', 'bar'), ), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), [], [], @@ -323,9 +333,9 @@ public function dataCondition(): array new Expr\BinaryOp\BooleanOr( new Expr\BinaryOp\BooleanAnd( new Expr\BooleanNot($this->createFunctionCall('is_int', 'foo')), - new Expr\BooleanNot($this->createFunctionCall('is_string', 'foo')) + new Expr\BooleanNot($this->createFunctionCall('is_string', 'foo')), ), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), [], ['$foo' => 'int|string'], @@ -334,18 +344,18 @@ public function dataCondition(): array new Expr\BinaryOp\BooleanAnd( new Expr\BinaryOp\BooleanOr( new Expr\BooleanNot($this->createFunctionCall('is_int', 'foo')), - new Expr\BooleanNot($this->createFunctionCall('is_string', 'foo')) + new Expr\BooleanNot($this->createFunctionCall('is_string', 'foo')), ), - $this->createFunctionCall('random') + $this->createFunctionCall('random'), ), - ['$foo' => '~*NEVER*'], + ['$foo' => 'mixed'], [], ], [ new Identical( new Variable('foo'), - new Expr\ConstFetch(new Name('true')) + new Expr\ConstFetch(new Name('true')), ), ['$foo' => 'true & ~' . self::FALSEY_TYPE_DESCRIPTION], ['$foo' => '~true'], @@ -353,7 +363,7 @@ public function dataCondition(): array [ new Identical( new Variable('foo'), - new Expr\ConstFetch(new Name('false')) + new Expr\ConstFetch(new Name('false')), ), ['$foo' => 'false & ~' . self::TRUTHY_TYPE_DESCRIPTION], ['$foo' => '~false'], @@ -361,7 +371,7 @@ public function dataCondition(): array [ new Identical( $this->createFunctionCall('is_int'), - new Expr\ConstFetch(new Name('true')) + new Expr\ConstFetch(new Name('true')), ), ['is_int($foo)' => 'true', '$foo' => 'int'], ['is_int($foo)' => '~true', '$foo' => '~int'], @@ -369,7 +379,7 @@ public function dataCondition(): array [ new Identical( $this->createFunctionCall('is_int'), - new Expr\ConstFetch(new Name('false')) + new Expr\ConstFetch(new Name('false')), ), ['is_int($foo)' => 'false', '$foo' => '~int'], ['$foo' => 'int', 'is_int($foo)' => '~false'], @@ -377,7 +387,7 @@ public function dataCondition(): array [ new Equal( $this->createFunctionCall('is_int'), - new Expr\ConstFetch(new Name('true')) + new Expr\ConstFetch(new Name('true')), ), ['$foo' => 'int'], ['$foo' => '~int'], @@ -385,7 +395,7 @@ public function dataCondition(): array [ new Equal( $this->createFunctionCall('is_int'), - new Expr\ConstFetch(new Name('false')) + new Expr\ConstFetch(new Name('false')), ), ['$foo' => '~int'], ['$foo' => 'int'], @@ -393,7 +403,7 @@ public function dataCondition(): array [ new Equal( new Variable('foo'), - new Expr\ConstFetch(new Name('false')) + new Expr\ConstFetch(new Name('false')), ), ['$foo' => self::SURE_NOT_TRUTHY], ['$foo' => self::SURE_NOT_FALSEY], @@ -401,7 +411,7 @@ public function dataCondition(): array [ new Equal( new Variable('foo'), - new Expr\ConstFetch(new Name('null')) + new Expr\ConstFetch(new Name('null')), ), ['$foo' => self::SURE_NOT_TRUTHY], ['$foo' => self::SURE_NOT_FALSEY], @@ -409,7 +419,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\Identical( new Variable('foo'), - new Variable('bar') + new Variable('bar'), ), ['$foo' => 'Bar', '$bar' => 'Bar'], [], @@ -435,11 +445,11 @@ public function dataCondition(): array new Arg(new Variable('foo')), new Arg(new Expr\ClassConstFetch( new Name('static'), - 'class' + 'class', )), ]), ['$foo' => 'static(DateTime)'], - ['$foo' => '~static(DateTime)'], + [], ], [ new FuncCall(new Name('is_a'), [ @@ -455,7 +465,7 @@ public function dataCondition(): array new Arg(new Variable('genericClassString')), ]), ['$foo' => 'Bar'], - ['$foo' => '~Bar'], + [], ], [ new FuncCall(new Name('is_a'), [ @@ -464,7 +474,7 @@ public function dataCondition(): array new Arg(new Expr\ConstFetch(new Name('true'))), ]), ['$foo' => 'class-string|Foo'], - ['$foo' => '~Foo'], + ['$foo' => '~class-string|Foo'], ], [ new FuncCall(new Name('is_a'), [ @@ -472,7 +482,7 @@ public function dataCondition(): array new Arg(new Variable('className')), new Arg(new Expr\ConstFetch(new Name('true'))), ]), - ['$foo' => 'class-string|object'], + ['$foo' => 'class-string|object'], [], ], [ @@ -482,7 +492,7 @@ public function dataCondition(): array new Arg(new Variable('unknown')), ]), ['$foo' => 'class-string|Foo'], - ['$foo' => '~Foo'], + ['$foo' => '~class-string|Foo'], ], [ new FuncCall(new Name('is_a'), [ @@ -490,13 +500,13 @@ public function dataCondition(): array new Arg(new Variable('className')), new Arg(new Variable('unknown')), ]), - ['$foo' => 'class-string|object'], + ['$foo' => 'class-string|object'], [], ], [ new Expr\Assign( new Variable('foo'), - new Variable('stringOrNull') + new Variable('stringOrNull'), ), ['$foo' => self::SURE_NOT_FALSEY], ['$foo' => self::SURE_NOT_TRUTHY], @@ -504,7 +514,7 @@ public function dataCondition(): array [ new Expr\Assign( new Variable('foo'), - new Variable('stringOrFalse') + new Variable('stringOrFalse'), ), ['$foo' => self::SURE_NOT_FALSEY], ['$foo' => self::SURE_NOT_TRUTHY], @@ -512,7 +522,7 @@ public function dataCondition(): array [ new Expr\Assign( new Variable('foo'), - new Variable('bar') + new Variable('bar'), ), ['$foo' => self::SURE_NOT_FALSEY], ['$foo' => self::SURE_NOT_TRUTHY], @@ -533,61 +543,76 @@ public function dataCondition(): array [ new Expr\BooleanNot(new Expr\Empty_(new Variable('stringOrNull'))), [ - '$stringOrNull' => '~false|null', - ], - [ - 'empty($stringOrNull)' => self::SURE_NOT_FALSEY, + '$stringOrNull' => '~0|0.0|\'\'|\'0\'|array{}|false|null', ], + [], ], [ new Expr\BinaryOp\Identical( new Variable('foo'), - new LNumber(123) + new LNumber(123), ), [ '$foo' => '123', - 123 => '123', ], ['$foo' => '~123'], ], [ new Expr\Empty_(new Variable('array')), + [], [ - '$array' => '~nonEmpty', + '$array' => '~0|0.0|\'\'|\'0\'|array{}|false|null', ], + ], + [ + new BooleanNot(new Expr\Empty_(new Variable('array'))), [ - '$array' => 'nonEmpty & ~false|null', + '$array' => '~0|0.0|\'\'|\'0\'|array{}|false|null', ], + [], ], [ - new BooleanNot(new Expr\Empty_(new Variable('array'))), + new FuncCall(new Name('count'), [ + new Arg(new Variable('array')), + ]), [ - '$array' => 'nonEmpty & ~false|null', + '$array' => 'non-empty-array', ], [ - '$array' => '~nonEmpty', + '$array' => '~non-empty-array', ], ], [ - new FuncCall(new Name('count'), [ + new BooleanNot(new FuncCall(new Name('count'), [ + new Arg(new Variable('array')), + ])), + [ + '$array' => '~non-empty-array', + ], + [ + '$array' => 'non-empty-array', + ], + ], + [ + new FuncCall(new Name('sizeof'), [ new Arg(new Variable('array')), ]), [ - '$array' => 'nonEmpty', + '$array' => 'non-empty-array', ], [ - '$array' => '~nonEmpty', + '$array' => '~non-empty-array', ], ], [ - new BooleanNot(new FuncCall(new Name('count'), [ + new BooleanNot(new FuncCall(new Name('sizeof'), [ new Arg(new Variable('array')), ])), [ - '$array' => '~nonEmpty', + '$array' => '~non-empty-array', ], [ - '$array' => 'nonEmpty', + '$array' => 'non-empty-array', ], ], [ @@ -612,9 +637,9 @@ public function dataCondition(): array new Equal( new Expr\Instanceof_( new Variable('foo'), - new Variable('className') + new Variable('className'), ), - new LNumber(1) + new LNumber(1), ), ['$foo' => 'object'], [], @@ -623,9 +648,9 @@ public function dataCondition(): array new Equal( new Expr\Instanceof_( new Variable('foo'), - new Variable('className') + new Variable('className'), ), - new LNumber(0) + new LNumber(0), ), [], [ @@ -636,7 +661,7 @@ public function dataCondition(): array new Expr\Isset_( [ new PropertyFetch(new Variable('foo'), new Identifier('bar')), - ] + ], ), [ '$foo' => 'object&hasProperty(bar) & ~null', @@ -650,7 +675,7 @@ public function dataCondition(): array new Expr\Isset_( [ new Expr\StaticPropertyFetch(new Name('Foo'), new VarLikeIdentifier('bar')), - ] + ], ), [ 'Foo::$bar' => '~null', @@ -662,7 +687,7 @@ public function dataCondition(): array [ new Identical( new Variable('barOrNull'), - new Expr\ConstFetch(new Name('null')) + new Expr\ConstFetch(new Name('null')), ), [ '$barOrNull' => 'null', @@ -675,9 +700,9 @@ public function dataCondition(): array new Identical( new Expr\Assign( new Variable('notNullBar'), - new Variable('barOrNull') + new Variable('barOrNull'), ), - new Expr\ConstFetch(new Name('null')) + new Expr\ConstFetch(new Name('null')), ), [ '$notNullBar' => 'null', @@ -689,7 +714,7 @@ public function dataCondition(): array [ new NotIdentical( new Variable('barOrNull'), - new Expr\ConstFetch(new Name('null')) + new Expr\ConstFetch(new Name('null')), ), [ '$barOrNull' => '~null', @@ -701,7 +726,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\Smaller( new Variable('n'), - new LNumber(3) + new LNumber(3), ), [ '$n' => 'mixed~int<3, max>|true', @@ -713,7 +738,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\Smaller( new Variable('n'), - new LNumber(PHP_INT_MIN) + new LNumber(PHP_INT_MIN), ), [ '$n' => 'mixed~int<' . PHP_INT_MIN . ', max>|true', @@ -725,7 +750,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\Greater( new Variable('n'), - new LNumber(PHP_INT_MAX) + new LNumber(PHP_INT_MAX), ), [ '$n' => 'mixed~bool|int|null', @@ -737,7 +762,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\SmallerOrEqual( new Variable('n'), - new LNumber(PHP_INT_MIN) + new LNumber(PHP_INT_MIN), ), [ '$n' => 'mixed~int<' . (PHP_INT_MIN + 1) . ', max>', @@ -749,7 +774,7 @@ public function dataCondition(): array [ new Expr\BinaryOp\GreaterOrEqual( new Variable('n'), - new LNumber(PHP_INT_MAX) + new LNumber(PHP_INT_MAX), ), [ '$n' => 'mixed~int|false|null', @@ -762,12 +787,12 @@ public function dataCondition(): array new Expr\BinaryOp\BooleanAnd( new Expr\BinaryOp\GreaterOrEqual( new Variable('n'), - new LNumber(3) + new LNumber(3), ), new Expr\BinaryOp\SmallerOrEqual( new Variable('n'), - new LNumber(5) - ) + new LNumber(5), + ), ), [ '$n' => 'mixed~int|int<6, max>|false|null', @@ -780,12 +805,12 @@ public function dataCondition(): array new Expr\BinaryOp\BooleanAnd( new Expr\Assign( new Variable('foo'), - new LNumber(1) + new LNumber(1), ), new Expr\BinaryOp\SmallerOrEqual( new Variable('n'), - new LNumber(5) - ) + new LNumber(5), + ), ), [ '$n' => 'mixed~int<6, max>', @@ -797,9 +822,9 @@ public function dataCondition(): array new NotIdentical( new Expr\Assign( new Variable('notNullBar'), - new Variable('barOrNull') + new Variable('barOrNull'), ), - new Expr\ConstFetch(new Name('null')) + new Expr\ConstFetch(new Name('null')), ), [ '$notNullBar' => '~null', @@ -811,7 +836,7 @@ public function dataCondition(): array [ new Identical( new Variable('barOrFalse'), - new Expr\ConstFetch(new Name('false')) + new Expr\ConstFetch(new Name('false')), ), [ '$barOrFalse' => 'false & ' . self::SURE_NOT_TRUTHY, @@ -824,9 +849,9 @@ public function dataCondition(): array new Identical( new Expr\Assign( new Variable('notFalseBar'), - new Variable('barOrFalse') + new Variable('barOrFalse'), ), - new Expr\ConstFetch(new Name('false')) + new Expr\ConstFetch(new Name('false')), ), [ '$notFalseBar' => 'false & ' . self::SURE_NOT_TRUTHY, @@ -838,7 +863,7 @@ public function dataCondition(): array [ new NotIdentical( new Variable('barOrFalse'), - new Expr\ConstFetch(new Name('false')) + new Expr\ConstFetch(new Name('false')), ), [ '$barOrFalse' => '~false', @@ -851,9 +876,9 @@ public function dataCondition(): array new NotIdentical( new Expr\Assign( new Variable('notFalseBar'), - new Variable('barOrFalse') + new Variable('barOrFalse'), ), - new Expr\ConstFetch(new Name('false')) + new Expr\ConstFetch(new Name('false')), ), [ '$notFalseBar' => '~false', @@ -866,9 +891,9 @@ public function dataCondition(): array new Expr\Instanceof_( new Expr\Assign( new Variable('notFalseBar'), - new Variable('barOrFalse') + new Variable('barOrFalse'), ), - new Name('Bar') + new Name('Bar'), ), [ '$notFalseBar' => 'Bar', @@ -886,7 +911,7 @@ public function dataCondition(): array new FuncCall(new Name('array_key_exists'), [ new Arg(new String_('bar')), new Arg(new Variable('array')), - ]) + ]), ), [ '$array' => 'array', @@ -904,7 +929,7 @@ public function dataCondition(): array new FuncCall(new Name('array_key_exists'), [ new Arg(new String_('bar')), new Arg(new Variable('array')), - ]) + ]), )), [ '$array' => '~hasOffset(\'bar\')|hasOffset(\'foo\')', @@ -935,6 +960,17 @@ public function dataCondition(): array ], [], ], + [ + new FuncCall(new Name('is_subclass_of'), [ + new Arg(new Variable('object')), + new Arg(new Variable('stringOrNull')), + new Arg(new Expr\ConstFetch(new Name('false'))), + ]), + [ + '$object' => 'object', + ], + [], + ], [ new FuncCall(new Name('is_subclass_of'), [ new Arg(new Variable('string')), @@ -946,11 +982,179 @@ public function dataCondition(): array ], [], ], + [ + new FuncCall(new Name('is_subclass_of'), [ + new Arg(new Variable('string')), + new Arg(new Variable('genericClassString')), + ]), + [ + '$string' => 'Bar|class-string', + ], + [], + ], + [ + new FuncCall(new Name('is_subclass_of'), [ + new Arg(new Variable('object')), + new Arg(new Variable('genericClassString')), + new Arg(new Expr\ConstFetch(new Name('false'))), + ]), + [ + '$object' => 'Bar', + ], + [], + ], + [ + new FuncCall(new Name('is_subclass_of'), [ + new Arg(new Variable('string')), + new Arg(new Variable('genericClassString')), + new Arg(new Expr\ConstFetch(new Name('false'))), + ]), + [ + '$string' => 'Bar', + ], + [], + ], + [ + new Expr\BinaryOp\BooleanOr( + new Expr\BinaryOp\BooleanAnd( + $this->createFunctionCall('is_string', 'a'), + new NotIdentical(new String_(''), new Variable('a')), + ), + new Identical(new Expr\ConstFetch(new Name('null')), new Variable('a')), + ), + ['$a' => 'non-empty-string|null'], + ['$a' => 'mixed~non-empty-string & ~null'], + ], + [ + new Expr\BinaryOp\BooleanOr( + new Expr\BinaryOp\BooleanAnd( + $this->createFunctionCall('is_string', 'a'), + new Expr\BinaryOp\Greater( + $this->createFunctionCall('strlen', 'a'), + new LNumber(0), + ), + ), + new Identical(new Expr\ConstFetch(new Name('null')), new Variable('a')), + ), + ['$a' => 'non-empty-string|null'], + ['$a' => 'mixed~non-empty-string & ~null'], + ], + [ + new Expr\BinaryOp\BooleanOr( + new Expr\BinaryOp\BooleanAnd( + $this->createFunctionCall('is_array', 'a'), + new Expr\BinaryOp\Greater( + $this->createFunctionCall('count', 'a'), + new LNumber(0), + ), + ), + new Identical(new Expr\ConstFetch(new Name('null')), new Variable('a')), + ), + ['$a' => 'non-empty-array|null'], + ['$a' => 'mixed~non-empty-array & ~null'], + ], + [ + new Expr\BinaryOp\BooleanAnd( + $this->createFunctionCall('is_array', 'foo'), + new Identical( + new FuncCall( + new Name('array_filter'), + [new Arg(new Variable('foo')), new Arg(new String_('is_string')), new Arg(new ConstFetch(new Name('ARRAY_FILTER_USE_KEY')))], + ), + new Variable('foo'), + ), + ), + [ + '$foo' => 'array', + 'array_filter($foo, \'is_string\', ARRAY_FILTER_USE_KEY)' => 'array', + ], + [], + ], + [ + new Expr\BinaryOp\BooleanAnd( + $this->createFunctionCall('is_array', 'foo'), + new Expr\BinaryOp\GreaterOrEqual( + new FuncCall( + new Name('count'), + [new Arg(new Variable('foo'))], + ), + new LNumber(2), + ), + ), + [ + '$foo' => 'non-empty-array', + 'count($foo)' => 'mixed~int|false|null', + ], + [], + ], + [ + new Expr\BinaryOp\BooleanAnd( + $this->createFunctionCall('is_array', 'foo'), + new Identical( + new FuncCall( + new Name('count'), + [new Arg(new Variable('foo'))], + ), + new LNumber(2), + ), + ), + [ + '$foo' => 'non-empty-array', + 'count($foo)' => '2', + ], + [], + ], + [ + new Expr\BinaryOp\BooleanAnd( + $this->createFunctionCall('is_string', 'foo'), + new NotIdentical( + new FuncCall( + new Name('strlen'), + [new Arg(new Variable('foo'))], + ), + new LNumber(0), + ), + ), + [ + '$foo' => 'non-empty-string', + 'strlen($foo)' => '~0', + ], + [ + '$foo' => 'mixed~non-empty-string', + ], + ], + [ + new Expr\BinaryOp\BooleanAnd( + $this->createFunctionCall('is_numeric', 'int'), + new Expr\BinaryOp\Equal( + new Variable('int'), + new Expr\Cast\Int_(new Variable('int')), + ), + ), + [ + '$int' => 'int', + '(int) $int' => 'int', + ], + [], + ], + [ + new Expr\BinaryOp\BooleanAnd( + $this->createFunctionCall('is_numeric', 'float'), + new Expr\BinaryOp\Equal( + new Variable('float'), + new Expr\Cast\Int_(new Variable('float')), + ), + ), + [ + '$float' => 'float', + '(int) $float' => 'int', + ], + [], + ], ]; } /** - * @param \PHPStan\Analyser\SpecifiedTypes $specifiedTypes * @return mixed[] */ private function toReadableResult(SpecifiedTypes $specifiedTypes): array diff --git a/tests/PHPStan/Analyser/TypeSpecifyingExtension-false.neon b/tests/PHPStan/Analyser/TypeSpecifyingExtension-false.neon new file mode 100644 index 0000000000..094662df08 --- /dev/null +++ b/tests/PHPStan/Analyser/TypeSpecifyingExtension-false.neon @@ -0,0 +1,13 @@ +services: + - + class: PHPStan\Tests\AssertionClassMethodTypeSpecifyingExtension + arguments: + nullContext: false + tags: + - phpstan.typeSpecifier.methodTypeSpecifyingExtension + - + class: PHPStan\Tests\AssertionClassStaticMethodTypeSpecifyingExtension + arguments: + nullContext: false + tags: + - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension diff --git a/tests/PHPStan/Analyser/TypeSpecifyingExtension-null.neon b/tests/PHPStan/Analyser/TypeSpecifyingExtension-null.neon new file mode 100644 index 0000000000..d208833ec3 --- /dev/null +++ b/tests/PHPStan/Analyser/TypeSpecifyingExtension-null.neon @@ -0,0 +1,13 @@ +services: + - + class: PHPStan\Tests\AssertionClassMethodTypeSpecifyingExtension + arguments: + nullContext: null + tags: + - phpstan.typeSpecifier.methodTypeSpecifyingExtension + - + class: PHPStan\Tests\AssertionClassStaticMethodTypeSpecifyingExtension + arguments: + nullContext: null + tags: + - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension diff --git a/tests/PHPStan/Analyser/TypeSpecifyingExtension-true.neon b/tests/PHPStan/Analyser/TypeSpecifyingExtension-true.neon new file mode 100644 index 0000000000..cd413d0751 --- /dev/null +++ b/tests/PHPStan/Analyser/TypeSpecifyingExtension-true.neon @@ -0,0 +1,13 @@ +services: + - + class: PHPStan\Tests\AssertionClassMethodTypeSpecifyingExtension + arguments: + nullContext: true + tags: + - phpstan.typeSpecifier.methodTypeSpecifyingExtension + - + class: PHPStan\Tests\AssertionClassStaticMethodTypeSpecifyingExtension + arguments: + nullContext: true + tags: + - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension diff --git a/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceFalseTest.php b/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceFalseTest.php new file mode 100644 index 0000000000..3378922993 --- /dev/null +++ b/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceFalseTest.php @@ -0,0 +1,37 @@ +gatherAssertTypes(__DIR__ . '/data/type-specifying-extensions-1-false.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/type-specifying-extensions-2-false.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/type-specifying-extensions-3-false.php'); + } + + /** + * @dataProvider dataTypeSpecifyingExtensionsFalse + * @param mixed ...$args + */ + public function testTypeSpecifyingExtensionsFalse( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/TypeSpecifyingExtension-false.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceNullTest.php b/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceNullTest.php new file mode 100644 index 0000000000..bc8276fb59 --- /dev/null +++ b/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceNullTest.php @@ -0,0 +1,37 @@ +gatherAssertTypes(__DIR__ . '/data/type-specifying-extensions-1-null.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/type-specifying-extensions-2-null.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/type-specifying-extensions-3-null.php'); + } + + /** + * @dataProvider dataTypeSpecifyingExtensionsNull + * @param mixed ...$args + */ + public function testTypeSpecifyingExtensionsNull( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/TypeSpecifyingExtension-null.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceTrueTest.php b/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceTrueTest.php new file mode 100644 index 0000000000..7478a93f2a --- /dev/null +++ b/tests/PHPStan/Analyser/TypeSpecifyingExtensionTypeInferenceTrueTest.php @@ -0,0 +1,37 @@ +gatherAssertTypes(__DIR__ . '/data/type-specifying-extensions-1-true.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/type-specifying-extensions-2-true.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/type-specifying-extensions-3-true.php'); + } + + /** + * @dataProvider dataTypeSpecifyingExtensionsTrue + * @param mixed ...$args + */ + public function testTypeSpecifyingExtensionsTrue( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/TypeSpecifyingExtension-true.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/classConstantStub.stub b/tests/PHPStan/Analyser/classConstantStub.stub new file mode 100644 index 0000000000..b88ee8b705 --- /dev/null +++ b/tests/PHPStan/Analyser/classConstantStub.stub @@ -0,0 +1,14 @@ +foo->foo(); } + public function setFoo(self $foo): void + { + $this->foo = $foo; + } + } diff --git a/tests/PHPStan/Analyser/data/TestDynamicReturnTypeExtensions.php b/tests/PHPStan/Analyser/data/TestDynamicReturnTypeExtensions.php new file mode 100644 index 0000000000..3fd9f8b0b4 --- /dev/null +++ b/tests/PHPStan/Analyser/data/TestDynamicReturnTypeExtensions.php @@ -0,0 +1,191 @@ +getName(), ['getByPrimary'], true); + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): \PHPStan\Type\Type + { + $args = $methodCall->args; + if (count($args) === 0) { + return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + } + + $arg = $args[0]->value; + if (!($arg instanceof \PhpParser\Node\Expr\ClassConstFetch)) { + return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + } + + if (!($arg->class instanceof \PhpParser\Node\Name)) { + return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + } + + return new ObjectType((string) $arg->class); + } + +} + +class OffsetGetDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension +{ + + public function getClass(): string + { + return \DynamicMethodReturnTypesNamespace\ComponentContainer::class; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'offsetGet'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + $args = $methodCall->args; + if (count($args) === 0) { + return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + } + + $argType = $scope->getType($args[0]->value); + if (!$argType instanceof ConstantStringType) { + return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + } + + return new ObjectType($argType->getValue()); + } + +} + +class CreateManagerForEntityDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension +{ + + public function getClass(): string + { + return \DynamicMethodReturnTypesNamespace\EntityManager::class; + } + + public function isStaticMethodSupported(MethodReflection $methodReflection): bool + { + return in_array($methodReflection->getName(), ['createManagerForEntity'], true); + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): \PHPStan\Type\Type + { + $args = $methodCall->args; + if (count($args) === 0) { + return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + } + + $arg = $args[0]->value; + if (!($arg instanceof \PhpParser\Node\Expr\ClassConstFetch)) { + return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + } + + if (!($arg->class instanceof \PhpParser\Node\Name)) { + return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + } + + return new ObjectType((string) $arg->class); + } + +} + +class ConstructDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension +{ + + public function getClass(): string + { + return \DynamicMethodReturnTypesNamespace\Foo::class; + } + + public function isStaticMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === '__construct'; + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): \PHPStan\Type\Type + { + return new ObjectWithoutClassType(); + } + +} + +class ConstructWithoutConstructor implements DynamicStaticMethodReturnTypeExtension +{ + + public function getClass(): string + { + return \DynamicMethodReturnTypesNamespace\FooWithoutConstructor::class; + } + + public function isStaticMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === '__construct'; + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): \PHPStan\Type\Type + { + return new ObjectWithoutClassType(); + } + +} + +class GetSelfDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { + + public function getClass(): string + { + return \DynamicMethodReturnCompoundTypes\Collection::class; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'getSelf'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + return new ObjectType(\DynamicMethodReturnCompoundTypes\Collection::class); + } + +} + +class FooGetSelf implements DynamicMethodReturnTypeExtension { + + public function getClass(): string + { + return \DynamicMethodReturnCompoundTypes\Foo::class; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'getSelf'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + return new ObjectType(\DynamicMethodReturnCompoundTypes\Foo::class); + } + +} diff --git a/tests/PHPStan/Analyser/data/array-accessable.php b/tests/PHPStan/Analyser/data/array-accessable.php index e04bd8b48b..c49d71ff41 100644 --- a/tests/PHPStan/Analyser/data/array-accessable.php +++ b/tests/PHPStan/Analyser/data/array-accessable.php @@ -34,6 +34,7 @@ public function returnSelfWithIterableInt(): self } + #[\ReturnTypeWillChange] public function offsetExists($offset) { @@ -44,11 +45,13 @@ public function offsetGet($offset): int } + #[\ReturnTypeWillChange] public function offsetSet($offset, $value) { } + #[\ReturnTypeWillChange] public function offsetUnset($offset) { diff --git a/tests/PHPStan/Analyser/data/array-column-php7.php b/tests/PHPStan/Analyser/data/array-column-php7.php new file mode 100644 index 0000000000..5b634d5146 --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-column-php7.php @@ -0,0 +1,22 @@ + $array */ + public function testConstantArray1(array $array): void + { + assertType('array', array_column($array, 'column', 'key')); + } + + /** @param array $array */ + public function testConstantArray2(array $array): void + { + assertType('array', array_column($array, 'column', 'key')); + } + +} diff --git a/tests/PHPStan/Analyser/data/array-column-php8.php b/tests/PHPStan/Analyser/data/array-column-php8.php new file mode 100644 index 0000000000..78d96aa109 --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-column-php8.php @@ -0,0 +1,22 @@ + $array */ + public function testConstantArray1(array $array): void + { + assertType('array<*NEVER*, string>', array_column($array, 'column', 'key')); + } + + /** @param array $array */ + public function testConstantArray2(array $array): void + { + assertType('array', array_column($array, 'column', 'key')); + } + +} diff --git a/tests/PHPStan/Analyser/data/array-column.php b/tests/PHPStan/Analyser/data/array-column.php new file mode 100644 index 0000000000..c76aaa017a --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-column.php @@ -0,0 +1,208 @@ +> $array */ + public function testArray1(array $array): void + { + assertType('array', array_column($array, 'column')); + assertType('array', array_column($array, 'column', 'key')); + assertType('array>', array_column($array, null, 'key')); + } + + /** @param non-empty-array> $array */ + public function testArray2(array $array): void + { + // Note: Array may still be empty! + assertType('array', array_column($array, 'column')); + } + + /** @param array{} $array */ + public function testArray3(array $array): void + { + assertType('array{}', array_column($array, 'column')); + assertType('array{}', array_column($array, 'column', 'key')); + assertType('array{}', array_column($array, null, 'key')); + } + + /** @param array> $array */ + public function testArray4(array $array): void + { + assertType('array', array_column($array, 'column', 'key')); + } + + /** @param array> $array */ + public function testArray5(array $array): void + { + assertType('array', array_column($array, 'column', 'key')); + } + + /** @param array> $array */ + public function testArray6(array $array): void + { + assertType('array', array_column($array, 'column', 'key')); + } + + /** @param array> $array */ + public function testArray7(array $array): void + { + assertType('array<\'\'|int, null>', array_column($array, 'column', 'key')); + } + + /** @param array> $array */ + public function testArray8(array $array): void + { + assertType('array', array_column($array, 'column', 'key')); + } + + /** @param array $array */ + public function testConstantArray1(array $array): void + { + assertType('array', array_column($array, 'column')); + assertType('array', array_column($array, 'column', 'key')); + assertType('array', array_column($array, null, 'key')); + } + + /** @param array $array */ + public function testConstantArray2(array $array): void + { + assertType('array{}', array_column($array, 'foo')); + assertType('array{}', array_column($array, 'foo', 'key')); + } + + /** @param array{array{column: string, key: 'bar'}} $array */ + public function testConstantArray3(array $array): void + { + assertType("array{string}", array_column($array, 'column')); + assertType("array{bar: string}", array_column($array, 'column', 'key')); + assertType("array{bar: array{column: string, key: 'bar'}}", array_column($array, null, 'key')); + } + + /** @param array{array{column: string, key: string}} $array */ + public function testConstantArray4(array $array): void + { + assertType("non-empty-array", array_column($array, 'column', 'key')); + assertType("non-empty-array", array_column($array, null, 'key')); + } + + /** @param array $array */ + public function testConstantArray5(array $array): void + { + assertType("array", array_column($array, 'column')); + assertType("array<'bar'|int, 'foo'>", array_column($array, 'column', 'key')); + assertType("array<'bar'|int, array{column?: 'foo', key?: 'bar'}>", array_column($array, null, 'key')); + } + + /** @param array $array */ + public function testConstantArray6(array $array): void + { + assertType('array', array_column($array, mt_rand(0, 1) === 0 ? 'column1' : 'column2')); + } + + /** @param non-empty-array $array */ + public function testConstantArray7(array $array): void + { + assertType('non-empty-array', array_column($array, 'column')); + assertType('non-empty-array', array_column($array, 'column', 'key')); + assertType('non-empty-array', array_column($array, null, 'key')); + } + + /** @param array $array */ + public function testConstantArray8(array $array): void + { + assertType('array', array_column($array, 'column', 'key')); + } + + /** @param array $array */ + public function testConstantArray9(array $array): void + { + assertType('array<0|1, string>', array_column($array, 'column', 'key')); + } + + /** @param array $array */ + public function testConstantArray10(array $array): void + { + assertType('array<1, string>', array_column($array, 'column', 'key')); + } + + /** @param array $array */ + public function testConstantArray11(array $array): void + { + assertType('array<\'\', string>', array_column($array, 'column', 'key')); + } + + // These cases aren't handled precisely and will return non-constant arrays. + + /** @param array{array{column?: 'foo', key: 'bar'}} $array */ + public function testImprecise1(array $array): void + { + assertType("array", array_column($array, 'column')); + assertType("array<'bar', 'foo'>", array_column($array, 'column', 'key')); + assertType("array{bar: array{column?: 'foo', key: 'bar'}}", array_column($array, null, 'key')); + } + + /** @param array{array{column: 'foo', key?: 'bar'}} $array */ + public function testImprecise2(array $array): void + { + assertType("non-empty-array<'bar'|int, 'foo'>", array_column($array, 'column', 'key')); + assertType("non-empty-array<'bar'|int, array{column: 'foo', key?: 'bar'}>", array_column($array, null, 'key')); + } + + /** @param array{array{column: 'foo', key: 'bar'}}|array> $array */ + public function testImprecise3(array $array): void + { + assertType('array', array_column($array, 'column')); + assertType('array', array_column($array, 'column', 'key')); + } + + /** @param array{0?: array{column: 'foo', key: 'bar'}} $array */ + public function testImprecise4(array $array): void + { + assertType("array", array_column($array, 'column')); + assertType("array<'bar', 'foo'>", array_column($array, 'column', 'key')); + } + + /** @param array $array */ + public function testImprecise5(array $array): void + { + assertType('array', array_column($array, 'nodeName')); + assertType('array', array_column($array, 'nodeName', 'tagName')); + assertType('array', array_column($array, null, 'tagName')); + assertType('array', array_column($array, 'foo')); + assertType('array', array_column($array, 'foo', 'tagName')); + assertType('array', array_column($array, 'nodeName', 'foo')); + assertType('array', array_column($array, null, 'foo')); + } + + /** @param non-empty-array $array */ + public function testObjects1(array $array): void + { + assertType('non-empty-array', array_column($array, 'nodeName')); + assertType('non-empty-array', array_column($array, 'nodeName', 'tagName')); + assertType('non-empty-array', array_column($array, null, 'tagName')); + assertType('array', array_column($array, 'foo')); + assertType('array', array_column($array, 'foo', 'tagName')); + assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); + assertType('non-empty-array', array_column($array, null, 'foo')); + } + + /** @param array{DOMElement} $array */ + public function testObjects2(array $array): void + { + assertType('array{string}', array_column($array, 'nodeName')); + assertType('non-empty-array', array_column($array, 'nodeName', 'tagName')); + assertType('non-empty-array', array_column($array, null, 'tagName')); + assertType('array', array_column($array, 'foo')); + assertType('array', array_column($array, 'foo', 'tagName')); + assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); + assertType('non-empty-array', array_column($array, null, 'foo')); + } + +} diff --git a/tests/PHPStan/Analyser/data/array-destructuring-types.php b/tests/PHPStan/Analyser/data/array-destructuring-types.php new file mode 100644 index 0000000000..7c310cf5d2 --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-destructuring-types.php @@ -0,0 +1,70 @@ +foo] = [1]; + assertType('1', $this->foo); + } + + public function doBar() + { + foreach ([1, 2, 3] as $key => $this->foo) { + assertType('0|1|2', $key); + assertType('1|2|3', $this->foo); + } + } + + public function doBaz() + { + foreach ([[1], [2], [3]] as $key => [$this->foo]) { + assertType('0|1|2', $key); + assertType('1|2|3', $this->foo); + } + } + + public function doLorem() + { + foreach ([[1]] as $key => [$this->foo]) { + assertType('0', $key); + assertType('1', $this->foo); + } + } + +} + +class Bar +{ + + public function doFoo() + { + + $matrix = $this->preprocessOpeningHours(); + if ($matrix === []) { + return null; + } + + /** @var string[][] $matrix */ + $matrix[] = end($matrix); + + assertType('array>', $matrix); + } + + /** + * @return string[][] + */ + private function preprocessOpeningHours(): array + { + return []; + } + +} diff --git a/tests/PHPStan/Analyser/data/array-filter-arrow-functions.php b/tests/PHPStan/Analyser/data/array-filter-arrow-functions.php new file mode 100644 index 0000000000..0c39643fc9 --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-filter-arrow-functions.php @@ -0,0 +1,43 @@ += 7.4 + +namespace ArrayFilter; + +use function PHPStan\Testing\assertType; + +/** + * @param array $list1 + * @param array $list2 + * @param array $list3 + */ +function alwaysEvaluatesToFalse(array $list1, array $list2, array $list3): void +{ + $filtered1 = array_filter($list1, static fn($item): bool => is_string($item)); + assertType('array{}', $filtered1); + + $filtered2 = array_filter($list2, static fn($key): bool => is_string($key), ARRAY_FILTER_USE_KEY); + assertType('array{}', $filtered2); + + $filtered3 = array_filter($list3, static fn($item, $key): bool => is_string($item) && is_string($key), ARRAY_FILTER_USE_BOTH); + assertType('array{}', $filtered3); +} + +/** + * @param array $map1 + * @param array $map2 + * @param array $map3 + * @param array $map4 + */ +function filtersString(array $map1, array $map2, array $map3, array $map4): void +{ + $filtered1 = array_filter($map1, static fn($item): bool => is_string($item)); + assertType('array', $filtered1); + + $filtered2 = array_filter($map2, static fn($key): bool => is_string($key), ARRAY_FILTER_USE_KEY); + assertType('array', $filtered2); + + $filtered3 = array_filter($map3, static fn($item, $key): bool => is_string($item) && is_string($key), ARRAY_FILTER_USE_BOTH); + assertType('array', $filtered3); + + $filtered4 = array_filter($map4, static fn($item): bool => is_string($item), ARRAY_FILTER_USE_BOTH); + assertType('array', $filtered4); +} diff --git a/tests/PHPStan/Analyser/data/array-filter-callables.php b/tests/PHPStan/Analyser/data/array-filter-callables.php new file mode 100644 index 0000000000..6855d1fe92 --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-filter-callables.php @@ -0,0 +1,43 @@ + $list1 + * @param array $list2 + * @param array $list3 + */ +function alwaysEvaluatesToFalse(array $list1, array $list2, array $list3): void +{ + $filtered1 = array_filter($list1, static function ($item): bool { return is_string($item); }); + assertType('array{}', $filtered1); + + $filtered2 = array_filter($list2, static function ($key): bool { return is_string($key); }, ARRAY_FILTER_USE_KEY); + assertType('array{}', $filtered2); + + $filtered3 = array_filter($list3, static function ($item, $key): bool { return is_string($item) && is_string($key); }, ARRAY_FILTER_USE_BOTH); + assertType('array{}', $filtered3); +} + +/** + * @param array $map1 + * @param array $map2 + * @param array $map3 + * @param array $map4 + */ +function filtersString(array $map1, array $map2, array $map3, array $map4): void +{ + $filtered1 = array_filter($map1, static function ($item): bool { return is_string($item); }); + assertType('array', $filtered1); + + $filtered2 = array_filter($map2, static function ($key): bool { return is_string($key); }, ARRAY_FILTER_USE_KEY); + assertType('array', $filtered2); + + $filtered3 = array_filter($map3, static function ($item, $key): bool { return is_string($item) && is_string($key); }, ARRAY_FILTER_USE_BOTH); + assertType('array', $filtered3); + + $filtered4 = array_filter($map4, static function ($item): bool { return is_string($item); }, ARRAY_FILTER_USE_BOTH); + assertType('array', $filtered4); +} diff --git a/tests/PHPStan/Analyser/data/array-filter-string-callables.php b/tests/PHPStan/Analyser/data/array-filter-string-callables.php new file mode 100644 index 0000000000..e143e3bc91 --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-filter-string-callables.php @@ -0,0 +1,79 @@ + $list1 + * @param array $list2 + * @param array $list3 + */ +function alwaysEvaluatesToFalse(array $list1, array $list2, array $list3): void +{ + $filtered1 = array_filter($list1, 'is_string'); + assertType('array{}', $filtered1); + + $filtered2 = array_filter($list2, 'is_string', ARRAY_FILTER_USE_KEY); + assertType('array{}', $filtered2); + + $filtered3 = array_filter($list3, 'is_string', ARRAY_FILTER_USE_BOTH); + assertType('array{}', $filtered3); +} + +/** + * @param array $map1 + * @param array $map2 + * @param array $map3 + */ +function filtersString(array $map1, array $map2, array $map3): void +{ + $filtered1 = array_filter($map1, 'is_string'); + assertType('array', $filtered1); + + $filtered2 = array_filter($map2, 'is_string', ARRAY_FILTER_USE_KEY); + assertType('array', $filtered2); + + $filtered3 = array_filter($map3, 'is_string', ARRAY_FILTER_USE_BOTH); + assertType('array', $filtered3); +} + +/** + * @param array $list1 + */ +function nonCallableStringIsIgnored(array $list1): void +{ + $filtered1 = array_filter($list1, 'foo'); + assertType('array', $filtered1); +} + +/** + * @param array $map1 + * @param array $map2 + */ +function nonBuiltInFunctionsAreNotSupportedYetAndThereforeIgnored(array $map1, array $map2): void +{ + $filtered1 = array_filter($map1, '\ArrayFilter\isString'); + assertType('array', $filtered1); + + $filtered2 = array_filter($map2, '\ArrayFilter\Filters::isString'); + assertType('array', $filtered2); +} + +/** + * @param mixed $value + */ +function isString($value): bool +{ + return is_string($value); +} + +class Filters { + /** + * @param mixed $value + */ + public static function isString($value): bool + { + return is_string($value); + } +} diff --git a/tests/PHPStan/Analyser/data/array-filter.php b/tests/PHPStan/Analyser/data/array-filter.php new file mode 100644 index 0000000000..f5d1e181fe --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-filter.php @@ -0,0 +1,37 @@ + $map1 + * @param array $map2 + * @param array $map3 + */ +function withoutCallback(array $map1, array $map2, array $map3): void +{ + $filtered1 = array_filter($map1); + assertType('array|int<1, max>|non-empty-string|true>', $filtered1); + + $filtered2 = array_filter($map2, null, ARRAY_FILTER_USE_KEY); + assertType('array|int<1, max>|non-empty-string|true>', $filtered2); + + $filtered3 = array_filter($map3, null, ARRAY_FILTER_USE_BOTH); + assertType('array|int<1, max>|non-empty-string|true>', $filtered3); +} diff --git a/tests/PHPStan/Analyser/data/array-flip.php b/tests/PHPStan/Analyser/data/array-flip.php new file mode 100644 index 0000000000..2275170d0d --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-flip.php @@ -0,0 +1,43 @@ +', $flip); +} + +/** + * @param mixed[] $list + */ +function foo3($list) +{ + $flip = array_flip($list); + + assertType('array', $flip); +} + +/** + * @param array $array + */ +function foo4($array) +{ + $flip = array_flip($array); + assertType('array<1|2|3, int>', $flip); +} + + +/** + * @param array<1|2|3, string> $array + */ +function foo5($array) +{ + $flip = array_flip($array); + assertType('array', $flip); +} diff --git a/tests/PHPStan/Analyser/data/array-functions.php b/tests/PHPStan/Analyser/data/array-functions.php index 752fe4fd07..15d7252487 100644 --- a/tests/PHPStan/Analyser/data/array-functions.php +++ b/tests/PHPStan/Analyser/data/array-functions.php @@ -37,7 +37,16 @@ }, 1); $filledIntegers = array_fill(0, 5, 1); +$emptyFilled = array_fill(3, 0, 'banana'); $filledIntegersWithKeys = array_fill_keys([0], 1); +/** @var negative-int $negInt */ +$filledAlwaysFalse = array_fill(0, $negInt, 1); +/** @var positive-int $posInt */ +$filledNonEmptyArray = array_fill(0, $posInt, 'foo'); +$filledNegativeConstAlwaysFalse = array_fill(0, -5, 1); +/** @var int<-3, 5> $maybeNegRange */ +$filledByMaybeNegativeRange = array_fill(0, $maybeNegRange, 1); +$filledByPositiveRange = array_fill(0, rand(3, 5), 1); $integerKeys = [ 1 => 'foo', diff --git a/tests/PHPStan/Analyser/data/array-is-list-type-specifying.php b/tests/PHPStan/Analyser/data/array-is-list-type-specifying.php new file mode 100644 index 0000000000..d3779c9f66 --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-is-list-type-specifying.php @@ -0,0 +1,35 @@ +', $foo); + } else { + assertType('array', $foo); + } +} + +$bar = [1, 2, 3]; + +if (array_is_list($bar)) { + assertType('array{1, 2, 3}', $bar); +} else { + assertType('*NEVER*', $bar); +} + +/** @var array $foo */ + +if (array_is_list($foo)) { + assertType('array', $foo); +} else { + assertType('array', $foo); +} + +$baz = []; + +if (array_is_list($baz)) { + assertType('array{}', $baz); +} diff --git a/tests/PHPStan/Analyser/data/array-map.php b/tests/PHPStan/Analyser/data/array-map.php new file mode 100644 index 0000000000..7845d56c8a --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-map.php @@ -0,0 +1,62 @@ + $array + */ +function foo(array $array): void { + $mapped = array_map( + static function(string $string): string { + return (string) $string; + }, + $array + ); + + assertType('array', $mapped); +} + +/** + * @param non-empty-array $array + */ +function foo2(array $array): void { + $mapped = array_map( + static function(string $string): string { + return (string) $string; + }, + $array + ); + + assertType('non-empty-array', $mapped); +} + +/** + * @param list $array + */ +function foo3(array $array): void { + $mapped = array_map( + static function(string $string): string { + return (string) $string; + }, + $array + ); + + assertType('array', $mapped); +} + +/** + * @param non-empty-list $array + */ +function foo4(array $array): void { + $mapped = array_map( + static function(string $string): string { + return (string) $string; + }, + $array + ); + + assertType('non-empty-array', $mapped); +} diff --git a/tests/PHPStan/Analyser/data/array-next.php b/tests/PHPStan/Analyser/data/array-next.php new file mode 100644 index 0000000000..97dc582e49 --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-next.php @@ -0,0 +1,104 @@ + $a + */ + public function doBaz(array $a) + { + assertType('string|false', next($a)); + } + +} + +class Foo2 +{ + + public function doFoo() + { + $array = []; + assertType('false', prev($array)); + } + + /** + * @param int[] $a + */ + public function doBar(array $a) + { + assertType('int|false', prev($a)); + } + + /** + * @param non-empty-array $a + */ + public function doBaz(array $a) + { + assertType('string|false', prev($a)); + } + +} + +interface HttpClientPoolItem +{ + public function isDisabled(): bool; +} + +final class RoundRobinClientPool +{ + /** + * @var HttpClientPoolItem[] + */ + protected $clientPool = []; + + protected function chooseHttpClient(): HttpClientPoolItem + { + $last = current($this->clientPool); + assertType(HttpClientPoolItem::class . '|false', $last); + + do { + $client = next($this->clientPool); + assertType(HttpClientPoolItem::class . '|false', $client); + + if (false === $client) { + $client = reset($this->clientPool); + assertType(HttpClientPoolItem::class . '|false', $client); + + if (false === $client) { + throw new \Exception(); + } + + assertType(HttpClientPoolItem::class, $client); + } + + assertType(HttpClientPoolItem::class, $client); + + // Case when there is only one and the last one has been disabled + if ($last === $client) { + assertType(HttpClientPoolItem::class, $client); + throw new \Exception(); + } + } while ($client->isDisabled()); + + return $client; + } +} diff --git a/tests/PHPStan/Analyser/data/array-plus.php b/tests/PHPStan/Analyser/data/array-plus.php new file mode 100644 index 0000000000..b6d8b99e2d --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-plus.php @@ -0,0 +1,20 @@ + string)", $slash); - assertType('array string)>', $dollar); + assertType('array{namespace/key: string}', $slash); + assertType('array', $dollar); } } diff --git a/tests/PHPStan/Analyser/data/array-spread.php b/tests/PHPStan/Analyser/data/array-spread.php index 65de15eaef..5bd12fdc38 100644 --- a/tests/PHPStan/Analyser/data/array-spread.php +++ b/tests/PHPStan/Analyser/data/array-spread.php @@ -6,8 +6,8 @@ class Foo { /** - * @param int[] $integersArray - * @param int[] $integersIterable + * @param array $integersArray + * @param array $integersIterable */ public function doFoo( array $integersArray, diff --git a/tests/PHPStan/Analyser/data/array-unpacking-string-keys.php b/tests/PHPStan/Analyser/data/array-unpacking-string-keys.php new file mode 100644 index 0000000000..be5371f8b1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-unpacking-string-keys.php @@ -0,0 +1,46 @@ + 0, ...['a' => 1], ...['b' => 2]]; + +assertType('array{a: 1, b: 2}', $foo); + +$bar = [1, ...['a' => 1], ...['b' => 2]]; + +assertType('array{0: 1, a: 1, b: 2}', $bar); + +/** + * @param array $a + * @param array $b + */ +function foo(array $a, array $b) +{ + $c = [...$a, ...$b]; + + assertType('non-empty-array', $c); +} + +/** + * @param array $a + * @param array $b + */ +function bar(array $a, array $b) +{ + $c = [...$a, ...$b]; + + assertType('non-empty-array', $c); +} + +/** + * @param array $a + * @param array $b + */ +function baz(array $a, array $b) +{ + $c = [...$a, ...$b]; + + assertType('non-empty-array', $c); +} diff --git a/tests/PHPStan/Analyser/data/array_map_multiple.php b/tests/PHPStan/Analyser/data/array_map_multiple.php new file mode 100644 index 0000000000..ce73048a46 --- /dev/null +++ b/tests/PHPStan/Analyser/data/array_map_multiple.php @@ -0,0 +1,36 @@ + $i], ['bar' => $s]); + assertType('non-empty-array', $result); + } + + /** + * @param non-empty-array $array + * @param non-empty-array $other + */ + public function arrayMapNull(array $array, array $other): void + { + assertType('array{}', array_map(null, [])); + assertType('array{foo: true}', array_map(null, ['foo' => true])); + assertType('non-empty-array', array_map(null, [1, 2, 3], [4, 5, 6])); + + assertType('non-empty-array', array_map(null, $array)); + assertType('non-empty-array', array_map(null, $array, $array)); + assertType('non-empty-array', array_map(null, $array, $other)); + } + +} diff --git a/tests/PHPStan/Analyser/data/arrow-function-types.php b/tests/PHPStan/Analyser/data/arrow-function-types.php index 3ea7bf5f07..77cbe12cf8 100644 --- a/tests/PHPStan/Analyser/data/arrow-function-types.php +++ b/tests/PHPStan/Analyser/data/arrow-function-types.php @@ -12,33 +12,33 @@ class Foo public function doFoo(): void { - array_map(fn(array $a): array => assertType('array(\'foo\' => string, \'bar\' => int)', $a), $this->arrayShapes); + array_map(fn(array $a): array => assertType('array{foo: string, bar: int}', $a), $this->arrayShapes); $a = array_map(fn(array $a) => $a, $this->arrayShapes); - assertType('array string, \'bar\' => int)>', $a); + assertType('array', $a); - array_map(fn($b) => assertType('array(\'foo\' => string, \'bar\' => int)', $b), $this->arrayShapes); + array_map(fn($b) => assertType('array{foo: string, bar: int}', $b), $this->arrayShapes); $b = array_map(fn($b) => $b['foo'], $this->arrayShapes); assertType('array', $b); } public function doBar(): void { - usort($this->arrayShapes, fn(array $a, array $b): int => assertType('array(\'foo\' => string, \'bar\' => int)', $a)); + usort($this->arrayShapes, fn(array $a, array $b): int => assertType('array{foo: string, bar: int}', $a)); } public function doBar2(): void { - usort($this->arrayShapes, fn (array $a, array $b): int => assertType('array(\'foo\' => string, \'bar\' => int)', $b)); + usort($this->arrayShapes, fn (array $a, array $b): int => assertType('array{foo: string, bar: int}', $b)); } public function doBaz(): void { - usort($this->arrayShapes, fn ($a, $b): int => assertType('array(\'foo\' => string, \'bar\' => int)', $a)); + usort($this->arrayShapes, fn ($a, $b): int => assertType('array{foo: string, bar: int}', $a)); } public function doBaz2(): void { - usort($this->arrayShapes, fn ($a, $b): int => assertType('array(\'foo\' => string, \'bar\' => int)', $b)); + usort($this->arrayShapes, fn ($a, $b): int => assertType('array{foo: string, bar: int}', $b)); } } diff --git a/tests/PHPStan/Analyser/data/assign-nested-arrays.php b/tests/PHPStan/Analyser/data/assign-nested-arrays.php index 08227f4a8e..31cecaa4d1 100644 --- a/tests/PHPStan/Analyser/data/assign-nested-arrays.php +++ b/tests/PHPStan/Analyser/data/assign-nested-arrays.php @@ -14,7 +14,7 @@ public function doFoo(int $i) $array[$i]['bar'] = 1; $array[$i]['baz'] = 2; - assertType('array 1, \'baz\' => 2)>&nonEmpty', $array); + assertType('non-empty-array', $array); } public function doBar(int $i, int $j) @@ -27,7 +27,7 @@ public function doBar(int $i, int $j) echo $array[$i][$j]['bar']; echo $array[$i][$j]['baz']; - assertType('array 1, \'baz\' => 2)>&nonEmpty>&nonEmpty', $array); + assertType('non-empty-array>', $array); } } diff --git a/tests/PHPStan/Analyser/data/bcmath-dynamic-return.php b/tests/PHPStan/Analyser/data/bcmath-dynamic-return.php index 485709cb35..e7be5836a4 100644 --- a/tests/PHPStan/Analyser/data/bcmath-dynamic-return.php +++ b/tests/PHPStan/Analyser/data/bcmath-dynamic-return.php @@ -21,16 +21,16 @@ \PHPStan\Testing\assertType('null', bcdiv('10', '0')); // Warning: Division by zero \PHPStan\Testing\assertType('null', bcdiv('10', '0.0')); // Warning: Division by zero \PHPStan\Testing\assertType('null', bcdiv('10', 0.0)); // Warning: Division by zero -\PHPStan\Testing\assertType('string&numeric', bcdiv('10', '1')); -\PHPStan\Testing\assertType('string&numeric', bcdiv('10', '-1')); -\PHPStan\Testing\assertType('string&numeric', bcdiv('10', '2', 0)); -\PHPStan\Testing\assertType('string&numeric', bcdiv('10', '2', 1)); -\PHPStan\Testing\assertType('string&numeric', bcdiv('10', $iNeg)); -\PHPStan\Testing\assertType('string&numeric', bcdiv('10', $iPos)); -\PHPStan\Testing\assertType('string&numeric', bcdiv($iPos, $iPos)); -\PHPStan\Testing\assertType('(string&numeric)|null', bcdiv('10', $mixed)); -\PHPStan\Testing\assertType('string&numeric', bcdiv('10', $iPos, $iPos)); -\PHPStan\Testing\assertType('string&numeric', bcdiv('10', $iUnknown)); +\PHPStan\Testing\assertType('numeric-string', bcdiv('10', '1')); +\PHPStan\Testing\assertType('numeric-string', bcdiv('10', '-1')); +\PHPStan\Testing\assertType('numeric-string', bcdiv('10', '2', 0)); +\PHPStan\Testing\assertType('numeric-string', bcdiv('10', '2', 1)); +\PHPStan\Testing\assertType('numeric-string', bcdiv('10', $iNeg)); +\PHPStan\Testing\assertType('numeric-string', bcdiv('10', $iPos)); +\PHPStan\Testing\assertType('numeric-string', bcdiv($iPos, $iPos)); +\PHPStan\Testing\assertType('numeric-string|null', bcdiv('10', $mixed)); +\PHPStan\Testing\assertType('numeric-string', bcdiv('10', $iPos, $iPos)); +\PHPStan\Testing\assertType('numeric-string', bcdiv('10', $iUnknown)); \PHPStan\Testing\assertType('null', bcdiv('10', $iPos, $nonNumeric)); // Warning: expects parameter 3 to be int, string given in \PHPStan\Testing\assertType('null', bcdiv('10', $nonNumeric)); // Warning: bcmath function argument is not well-formed @@ -39,18 +39,18 @@ \PHPStan\Testing\assertType('null', bcmod('10', '0')); \PHPStan\Testing\assertType('null', bcmod($iPos, '0')); // Warning: Division by zero \PHPStan\Testing\assertType('null', bcmod('10', $nonNumeric)); -\PHPStan\Testing\assertType('string&numeric', bcmod('10', '1')); -\PHPStan\Testing\assertType('string&numeric', bcmod('10', '2', 0)); -\PHPStan\Testing\assertType('string&numeric', bcmod('5.7', '1.3', 1)); -\PHPStan\Testing\assertType('string&numeric', bcmod('10', 2.2)); -\PHPStan\Testing\assertType('string&numeric', bcmod('10', $iUnknown)); -\PHPStan\Testing\assertType('string&numeric', bcmod('10', '-1')); -\PHPStan\Testing\assertType('string&numeric', bcmod($iPos, '-1')); -\PHPStan\Testing\assertType('string&numeric', bcmod('10', $iNeg)); -\PHPStan\Testing\assertType('string&numeric', bcmod('10', $iPos)); -\PHPStan\Testing\assertType('string&numeric', bcmod('10', -$iNeg)); -\PHPStan\Testing\assertType('string&numeric', bcmod('10', -$iPos)); -\PHPStan\Testing\assertType('(string&numeric)|null', bcmod('10', $mixed)); +\PHPStan\Testing\assertType('numeric-string', bcmod('10', '1')); +\PHPStan\Testing\assertType('numeric-string', bcmod('10', '2', 0)); +\PHPStan\Testing\assertType('numeric-string', bcmod('5.7', '1.3', 1)); +\PHPStan\Testing\assertType('numeric-string', bcmod('10', 2.2)); +\PHPStan\Testing\assertType('numeric-string', bcmod('10', $iUnknown)); +\PHPStan\Testing\assertType('numeric-string', bcmod('10', '-1')); +\PHPStan\Testing\assertType('numeric-string', bcmod($iPos, '-1')); +\PHPStan\Testing\assertType('numeric-string', bcmod('10', $iNeg)); +\PHPStan\Testing\assertType('numeric-string', bcmod('10', $iPos)); +\PHPStan\Testing\assertType('numeric-string', bcmod('10', -$iNeg)); +\PHPStan\Testing\assertType('numeric-string', bcmod('10', -$iPos)); +\PHPStan\Testing\assertType('numeric-string|null', bcmod('10', $mixed)); // bcpowmod ( string $base , string $exponent , string $modulus [, int $scale = 0 ] ) : string // Returns the result as a numeric-string, or FALSE if modulus is 0 or exponent is negative. @@ -66,32 +66,32 @@ \PHPStan\Testing\assertType('false', bcpowmod('10', '2', '0')); // modulus is 0 \PHPStan\Testing\assertType('false', bcpowmod('10', 2.3, '0')); // modulus is 0 \PHPStan\Testing\assertType('false', bcpowmod('10', '0', '0')); // modulus is 0 -\PHPStan\Testing\assertType('string&numeric', bcpowmod('10', '0', '-2')); -\PHPStan\Testing\assertType('string&numeric', bcpowmod('10', '2', '2')); -\PHPStan\Testing\assertType('string&numeric', bcpowmod('10', $iUnknown, '2')); -\PHPStan\Testing\assertType('string&numeric', bcpowmod($iPos, '2', '2')); -\PHPStan\Testing\assertType('(string&numeric)|false', bcpowmod('10', $mixed, $mixed)); -\PHPStan\Testing\assertType('string&numeric', bcpowmod('10', '2', '2')); -\PHPStan\Testing\assertType('string&numeric', bcpowmod('10', -$iNeg, '2')); -\PHPStan\Testing\assertType('string&numeric', bcpowmod('10', $nonNumeric, '2')); // Warning: bcmath function argument is not well-formed -\PHPStan\Testing\assertType('(string&numeric)|false', bcpowmod('10', $iUnknown, $iUnknown)); +\PHPStan\Testing\assertType('numeric-string', bcpowmod('10', '0', '-2')); +\PHPStan\Testing\assertType('numeric-string', bcpowmod('10', '2', '2')); +\PHPStan\Testing\assertType('numeric-string', bcpowmod('10', $iUnknown, '2')); +\PHPStan\Testing\assertType('numeric-string', bcpowmod($iPos, '2', '2')); +\PHPStan\Testing\assertType('numeric-string|false', bcpowmod('10', $mixed, $mixed)); +\PHPStan\Testing\assertType('numeric-string', bcpowmod('10', '2', '2')); +\PHPStan\Testing\assertType('numeric-string', bcpowmod('10', -$iNeg, '2')); +\PHPStan\Testing\assertType('numeric-string', bcpowmod('10', $nonNumeric, '2')); // Warning: bcmath function argument is not well-formed +\PHPStan\Testing\assertType('numeric-string|false', bcpowmod('10', $iUnknown, $iUnknown)); // bcsqrt ( string $operand [, int $scale = 0 ] ) : string // Returns the square root as a numeric-string, or NULL if operand is negative. -\PHPStan\Testing\assertType('string&numeric', bcsqrt('10', $iNeg)); -\PHPStan\Testing\assertType('string&numeric', bcsqrt('10', 1)); -\PHPStan\Testing\assertType('string&numeric', bcsqrt('0.00', 1)); -\PHPStan\Testing\assertType('string&numeric', bcsqrt(0.0, 1)); -\PHPStan\Testing\assertType('string&numeric', bcsqrt('0', 1)); -\PHPStan\Testing\assertType('(string&numeric)|null', bcsqrt($iUnknown, $iUnknown)); -\PHPStan\Testing\assertType('string&numeric', bcsqrt('10', $iPos)); +\PHPStan\Testing\assertType('numeric-string', bcsqrt('10', $iNeg)); +\PHPStan\Testing\assertType('numeric-string', bcsqrt('10', 1)); +\PHPStan\Testing\assertType('numeric-string', bcsqrt('0.00', 1)); +\PHPStan\Testing\assertType('numeric-string', bcsqrt(0.0, 1)); +\PHPStan\Testing\assertType('numeric-string', bcsqrt('0', 1)); +\PHPStan\Testing\assertType('numeric-string|null', bcsqrt($iUnknown, $iUnknown)); +\PHPStan\Testing\assertType('numeric-string', bcsqrt('10', $iPos)); \PHPStan\Testing\assertType('null', bcsqrt('-10', 0)); // Warning: Square root of negative number \PHPStan\Testing\assertType('null', bcsqrt($iNeg, 0)); \PHPStan\Testing\assertType('null', bcsqrt('10', $nonNumeric)); // Warning: Second argument must be ?int (Fatal in PHP8) -\PHPStan\Testing\assertType('string&numeric', bcsqrt('10')); -\PHPStan\Testing\assertType('(string&numeric)|null', bcsqrt($iUnknown)); +\PHPStan\Testing\assertType('numeric-string', bcsqrt('10')); +\PHPStan\Testing\assertType('numeric-string|null', bcsqrt($iUnknown)); \PHPStan\Testing\assertType('null', bcsqrt('-10')); // Warning: Square root of negative number -\PHPStan\Testing\assertType('(string&numeric)|null', bcsqrt($nonNumeric, -1)); // Warning: bcmath function argument is not well-formed -\PHPStan\Testing\assertType('(string&numeric)|null', bcsqrt('10', $mixed)); -\PHPStan\Testing\assertType('string&numeric', bcsqrt($iPos)); +\PHPStan\Testing\assertType('numeric-string|null', bcsqrt($nonNumeric, -1)); // Warning: bcmath function argument is not well-formed +\PHPStan\Testing\assertType('numeric-string|null', bcsqrt('10', $mixed)); +\PHPStan\Testing\assertType('numeric-string', bcsqrt($iPos)); diff --git a/tests/PHPStan/Analyser/data/bug-1283.php b/tests/PHPStan/Analyser/data/bug-1283.php index fb1e5a2e0e..058c739bb9 100644 --- a/tests/PHPStan/Analyser/data/bug-1283.php +++ b/tests/PHPStan/Analyser/data/bug-1283.php @@ -21,7 +21,7 @@ function (array $levels): void { throw new \UnexpectedValueException(sprintf('Unsupported level `%s`', $level)); } - assertType('array(0 => 1, ?1 => 3)', $allowedElements); + assertType('array{0: 1, 1?: 3}', $allowedElements); assertVariableCertainty(TrinaryLogic::createYes(), $allowedElements); } }; diff --git a/tests/PHPStan/Analyser/data/bug-1516.php b/tests/PHPStan/Analyser/data/bug-1516.php new file mode 100644 index 0000000000..d62795a24b --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-1516.php @@ -0,0 +1,37 @@ + 'barr', + 'ftt' => [] + ]; + + foreach ($a as $k => $b) { + $str = 'toto'; + assertType('\'toto\'|array{}', $out[$k]); + + if (is_array($b)) { + // $out[$k] is redefined there before the array_merge + assertType('\'toto\'|array{}', $out[$k]); + $out[$k] = []; + assertType('array{}', $out[$k]); + $out[$k] = array_merge($out[$k], []); + assertType('array{}', $out[$k]); + + } else { + // I think phpstan takes this definition as a string and takes no account of the foreach + $out[$k] = $str; + assertType('\'toto\'', $out[$k]); + } + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-1597.php b/tests/PHPStan/Analyser/data/bug-1597.php index 5023defbb8..def66f078e 100644 --- a/tests/PHPStan/Analyser/data/bug-1597.php +++ b/tests/PHPStan/Analyser/data/bug-1597.php @@ -7,6 +7,9 @@ $date = ''; try { + if (rand(0,1) === 0) { + throw new \Exception(); + } $date = new \DateTime($date); } catch (\Exception $e) { assertType('\'\'', $date); diff --git a/tests/PHPStan/Analyser/data/bug-1861.php b/tests/PHPStan/Analyser/data/bug-1861.php new file mode 100644 index 0000000000..4d5335a67c --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-1861.php @@ -0,0 +1,29 @@ +children)) { + case 0: + assertType('array{}', $this->children); + break; + case 1: + assertType('non-empty-array<' . self::class . '>', $this->children); + assertType(self::class, reset($this->children)); + break; + default: + assertType('non-empty-array<' . self::class . '>', $this->children); + break; + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-1870.php b/tests/PHPStan/Analyser/data/bug-1870.php new file mode 100644 index 0000000000..2010b6cfc1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-1870.php @@ -0,0 +1,23 @@ + $this->getArrayOrNull(), 'b' => $this->getArrayOrNull(), ]; - assertType('array(\'a\' => array|null, \'b\' => array|null)', $arr); + assertType('array{a: array|null, b: array|null}', $arr); $cond = isset($arr['a']) && isset($arr['b']); assertType('bool', $cond); diff --git a/tests/PHPStan/Analyser/data/bug-2001.php b/tests/PHPStan/Analyser/data/bug-2001.php index 01ff5a4113..22db42086a 100644 --- a/tests/PHPStan/Analyser/data/bug-2001.php +++ b/tests/PHPStan/Analyser/data/bug-2001.php @@ -9,28 +9,28 @@ class HelloWorld public function parseUrl(string $url): string { $parsedUrl = parse_url(urldecode($url)); - assertType('array(?\'scheme\' => string, ?\'host\' => string, ?\'port\' => int, ?\'user\' => string, ?\'pass\' => string, ?\'path\' => string, ?\'query\' => string, ?\'fragment\' => string)|false', $parsedUrl); + assertType('array{scheme?: string, host?: string, port?: int, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', $parsedUrl); if (array_key_exists('host', $parsedUrl)) { - assertType('array(?\'scheme\' => string, \'host\' => string, ?\'port\' => int, ?\'user\' => string, ?\'pass\' => string, ?\'path\' => string, ?\'query\' => string, ?\'fragment\' => string)', $parsedUrl); + assertType('array{scheme?: string, host: string, port?: int, user?: string, pass?: string, path?: string, query?: string, fragment?: string}', $parsedUrl); throw new \RuntimeException('Absolute URLs are prohibited for the redirectTo parameter.'); } - assertType('array(?\'scheme\' => string, ?\'port\' => int, ?\'user\' => string, ?\'pass\' => string, ?\'path\' => string, ?\'query\' => string, ?\'fragment\' => string)|false', $parsedUrl); + assertType('array{scheme?: string, port?: int, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', $parsedUrl); $redirectUrl = $parsedUrl['path']; if (array_key_exists('query', $parsedUrl)) { - assertType('array(?\'scheme\' => string, ?\'port\' => int, ?\'user\' => string, ?\'pass\' => string, ?\'path\' => string, \'query\' => string, ?\'fragment\' => string)', $parsedUrl); + assertType('array{scheme?: string, port?: int, user?: string, pass?: string, path?: string, query: string, fragment?: string}', $parsedUrl); $redirectUrl .= '?' . $parsedUrl['query']; } if (array_key_exists('fragment', $parsedUrl)) { - assertType('array(?\'scheme\' => string, ?\'port\' => int, ?\'user\' => string, ?\'pass\' => string, ?\'path\' => string, ?\'query\' => string, \'fragment\' => string)', $parsedUrl); + assertType('array{scheme?: string, port?: int, user?: string, pass?: string, path?: string, query?: string, fragment: string}', $parsedUrl); $redirectUrl .= '#' . $parsedUrl['query']; } - assertType('array(?\'scheme\' => string, ?\'port\' => int, ?\'user\' => string, ?\'pass\' => string, ?\'path\' => string, ?\'query\' => string, ?\'fragment\' => string)|false', $parsedUrl); + assertType('array{scheme?: string, port?: int, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', $parsedUrl); return $redirectUrl; } @@ -46,6 +46,6 @@ public function doFoo(int $i) $a = ['d' => $i]; } - assertType('array(\'a\' => int, ?\'b\' => int)|array(\'d\' => int)', $a); + assertType('array{a: int, b?: int}|array{d: int}', $a); } } diff --git a/tests/PHPStan/Analyser/data/bug-2112.php b/tests/PHPStan/Analyser/data/bug-2112.php index 0a4a0a5bde..756c061b5e 100644 --- a/tests/PHPStan/Analyser/data/bug-2112.php +++ b/tests/PHPStan/Analyser/data/bug-2112.php @@ -19,7 +19,7 @@ public function doBar(): void $foos[0] = null; assertType('null', $foos[0]); - assertType('array&nonEmpty', $foos); + assertType('non-empty-array', $foos); } /** @return self[] */ @@ -35,7 +35,7 @@ public function doBars(): void $foos[0] = null; assertType('null', $foos[0]); - assertType('array&nonEmpty', $foos); + assertType('non-empty-array', $foos); } } diff --git a/tests/PHPStan/Analyser/data/bug-2142.php b/tests/PHPStan/Analyser/data/bug-2142.php new file mode 100644 index 0000000000..64e39ecd25 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-2142.php @@ -0,0 +1,99 @@ + 0) { + assertType('non-empty-array', $arr); + } + } + + /** + * @param string[] $arr + */ + function doFoo2(array $arr): void + { + if (count($arr) != 0) { + assertType('non-empty-array', $arr); + } + } + + /** + * @param string[] $arr + */ + function doFoo3(array $arr): void + { + if (count($arr) == 1) { + assertType('non-empty-array', $arr); + } + } + + /** + * @param string[] $arr + */ + function doFoo4(array $arr): void + { + if ($arr != []) { + assertType('non-empty-array', $arr); + } + } + + /** + * @param string[] $arr + */ + function doFoo5(array $arr): void + { + if (sizeof($arr) !== 0) { + assertType('non-empty-array', $arr); + } + } + + /** + * @param string[] $arr + */ + function doFoo6(array $arr): void + { + if (count($arr) !== 0) { + assertType('non-empty-array', $arr); + } + } + + + /** + * @param string[] $arr + */ + function doFoo7(array $arr): void + { + if (!empty($arr)) { + assertType('non-empty-array', $arr); + } + } + + /** + * @param string[] $arr + */ + function doFoo8(array $arr): void + { + if (count($arr) === 1) { + assertType('non-empty-array', $arr); + } + } + + + /** + * @param string[] $arr + */ + function doFoo9(array $arr): void + { + if ($arr !== []) { + assertType('non-empty-array', $arr); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-2232.php b/tests/PHPStan/Analyser/data/bug-2232.php index 3f1613c725..7464edf141 100644 --- a/tests/PHPStan/Analyser/data/bug-2232.php +++ b/tests/PHPStan/Analyser/data/bug-2232.php @@ -35,5 +35,5 @@ function () { $data['b5'] = "env"; } - assertType('array(\'a1\' => \'a\', \'a2\' => \'b\', \'a3\' => \'c\', \'a4\' => array(\'name\' => \'dsfs\', \'version\' => \'fdsfs\'), ?\'b1\' => \'hello\', ?\'b2\' => \'hello\', ?\'b3\' => \'hello\', ?\'b4\' => \'goodbye\', ?\'b5\' => \'env\')', $data); + assertType('array{a1: \'a\', a2: \'b\', a3: \'c\', a4: array{name: \'dsfs\', version: \'fdsfs\'}, b1?: \'hello\', b2?: \'hello\', b3?: \'hello\', b4?: \'goodbye\', b5?: \'env\'}', $data); }; diff --git a/tests/PHPStan/Analyser/data/bug-2378.php b/tests/PHPStan/Analyser/data/bug-2378.php index ab58f7a28c..d5984a6364 100644 --- a/tests/PHPStan/Analyser/data/bug-2378.php +++ b/tests/PHPStan/Analyser/data/bug-2378.php @@ -14,8 +14,8 @@ public function doFoo( float $f ): void { - assertType('array(\'a\', \'b\', \'c\', \'d\')', range('a', 'd')); - assertType('array(\'a\', \'c\', \'e\', \'g\', \'i\')', range('a', 'i', 2)); + assertType('array{\'a\', \'b\', \'c\', \'d\'}', range('a', 'd')); + assertType('array{\'a\', \'c\', \'e\', \'g\', \'i\'}', range('a', 'i', 2)); assertType('array', range($s, $s)); } diff --git a/tests/PHPStan/Analyser/data/bug-2420.php b/tests/PHPStan/Analyser/data/bug-2420.php new file mode 100644 index 0000000000..8b3d5b4809 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-2420.php @@ -0,0 +1,33 @@ + false, + 1 => false, + ]; + + public function sayHello(int $key): void + { + $config = self::CONFIG[$key] ?? true; + assertType('bool', $config); + } +} + +class HelloWorld2 +{ + const CONFIG = [ + 0 => ['foo' => false], + 1 => ['foo' => false], + ]; + + public function sayHello(int $key): void + { + $config = self::CONFIG[$key]['foo'] ?? true; + assertType('bool', $config); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-2600-php8.php b/tests/PHPStan/Analyser/data/bug-2600-php8.php new file mode 100644 index 0000000000..5df2b73736 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-2600-php8.php @@ -0,0 +1,88 @@ +', $x); + } + + /** + * @param mixed ...$x + */ + public function doLorem(...$x) { + assertType('array', $x); + } + + public function doIpsum($x = null) { + $args = func_get_args(); + assertType('mixed', $x); + assertType('array', $args); + } +} + +class Bar +{ + /** + * @param string ...$x + */ + public function doFoo($x = null) { + $args = func_get_args(); + assertType('string|null', $x); + assertType('array', $args); + } + + /** + * @param string ...$x + */ + public function doBar($x = null) { + assertType('string|null', $x); + } + + /** + * @param string $x + */ + public function doBaz(...$x) { + assertType('array', $x); + } + + /** + * @param string ...$x + */ + public function doLorem(...$x) { + assertType('array', $x); + } +} + +function foo($x, string ...$y): void +{ + assertType('mixed', $x); + assertType('array', $y); +} + +function ($x, string ...$y): void { + assertType('mixed', $x); + assertType('array', $y); +}; diff --git a/tests/PHPStan/Analyser/data/bug-2648.php b/tests/PHPStan/Analyser/data/bug-2648.php index 23797170a1..11087dfbaa 100644 --- a/tests/PHPStan/Analyser/data/bug-2648.php +++ b/tests/PHPStan/Analyser/data/bug-2648.php @@ -37,6 +37,8 @@ public function doBar(array $list): void assertType('int<0, max>', count($list)); if (count($list) === 1) { + assertType('1', count($list)); + $list[] = false; assertType('int<1, max>', count($list)); break; } diff --git a/tests/PHPStan/Analyser/data/bug-2676.php b/tests/PHPStan/Analyser/data/bug-2676.php index 4daa2b5552..30723db8a9 100644 --- a/tests/PHPStan/Analyser/data/bug-2676.php +++ b/tests/PHPStan/Analyser/data/bug-2676.php @@ -39,7 +39,7 @@ function (Wallet $wallet): void assertType('DoctrineIntersectionTypeIsSupertypeOf\Collection&iterable', $bankAccounts); foreach ($bankAccounts as $key => $bankAccount) { - assertType('(int|string)', $key); + assertType('mixed', $key); assertType('Bug2676\BankAccount', $bankAccount); } }; diff --git a/tests/PHPStan/Analyser/data/bug-2677.php b/tests/PHPStan/Analyser/data/bug-2677.php index aaf1201b2f..22656ae4a3 100644 --- a/tests/PHPStan/Analyser/data/bug-2677.php +++ b/tests/PHPStan/Analyser/data/bug-2677.php @@ -41,9 +41,9 @@ function () O::class, P::class, ]; - assertType('array(\'Bug2677\\\\A\', \'Bug2677\\\\B\', \'Bug2677\\\\C\', \'Bug2677\\\\D\', \'Bug2677\\\\E\', \'Bug2677\\\\F\', \'Bug2677\\\\G\', \'Bug2677\\\\H\', \'Bug2677\\\\I\', \'Bug2677\\\\J\', \'Bug2677\\\\K\', \'Bug2677\\\\L\', \'Bug2677\\\\M\', \'Bug2677\\\\N\', \'Bug2677\\\\O\', \'Bug2677\\\\P\')', $classes); + assertType('array{\'Bug2677\\\\A\', \'Bug2677\\\\B\', \'Bug2677\\\\C\', \'Bug2677\\\\D\', \'Bug2677\\\\E\', \'Bug2677\\\\F\', \'Bug2677\\\\G\', \'Bug2677\\\\H\', \'Bug2677\\\\I\', \'Bug2677\\\\J\', \'Bug2677\\\\K\', \'Bug2677\\\\L\', \'Bug2677\\\\M\', \'Bug2677\\\\N\', \'Bug2677\\\\O\', \'Bug2677\\\\P\'}', $classes); foreach ($classes as $class) { - assertType('class-string', $class); + assertType('\'Bug2677\\\\A\'|\'Bug2677\\\\B\'|\'Bug2677\\\\C\'|\'Bug2677\\\\D\'|\'Bug2677\\\\E\'|\'Bug2677\\\\F\'|\'Bug2677\\\\G\'|\'Bug2677\\\\H\'|\'Bug2677\\\\I\'|\'Bug2677\\\\J\'|\'Bug2677\\\\K\'|\'Bug2677\\\\L\'|\'Bug2677\\\\M\'|\'Bug2677\\\\N\'|\'Bug2677\\\\O\'|\'Bug2677\\\\P\'', $class); } }; diff --git a/tests/PHPStan/Analyser/data/bug-2718.php b/tests/PHPStan/Analyser/data/bug-2718.php new file mode 100644 index 0000000000..cbd4640939 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-2718.php @@ -0,0 +1,41 @@ + 'a', 'user_id' => 'id1'], + ['group_id' => 'a', 'user_id' => 'id2'], + ['group_id' => 'a', 'user_id' => 'id3'], + ['group_id' => 'b', 'user_id' => 'id4'], + ['group_id' => 'b', 'user_id' => 'id5'], + ['group_id' => 'b', 'user_id' => 'id6'], + ]; + }; + + $orders = $fun(); + + $result = []; + foreach ($orders as $order) { + assertType('bool', isset($result[$order['group_id']]['users'])); + if (isset($result[$order['group_id']]['users'])) { + $result[$order['group_id']]['users'][] = $order['user_id']; + continue; + } + + $result[$order['group_id']] = [ + 'users' => [ + $order['user_id'], + ], + ]; + } + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-2733.php b/tests/PHPStan/Analyser/data/bug-2733.php index d40acdc032..f18f5053d9 100644 --- a/tests/PHPStan/Analyser/data/bug-2733.php +++ b/tests/PHPStan/Analyser/data/bug-2733.php @@ -18,7 +18,7 @@ public function doSomething(array $data): void } } - assertType('array(\'id\' => int, \'name\' => string)', $data); + assertType('array{id: int, name: string}', $data); } } diff --git a/tests/PHPStan/Analyser/data/bug-2760.php b/tests/PHPStan/Analyser/data/bug-2760.php new file mode 100644 index 0000000000..72e2e6a516 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-2760.php @@ -0,0 +1,24 @@ +', $i); if ($tokens[$i]['code'] !== 1) { assertType('mixed~1', $tokens[$i]['code']); $i++; - assertType('int', $i); + assertType('int<1, max>', $i); assertType('mixed', $tokens[$i]['code']); continue; } assertType('1', $tokens[$i]['code']); $i++; - assertType('int', $i); + assertType('int<1, max>', $i); assertType('mixed', $tokens[$i]['code']); if ($tokens[$i]['code'] !== 2) { assertType('mixed~2', $tokens[$i]['code']); $i++; - assertType('int', $i); + assertType('int<2, max>', $i); continue; } assertType('2', $tokens[$i]['code']); diff --git a/tests/PHPStan/Analyser/data/bug-2899.php b/tests/PHPStan/Analyser/data/bug-2899.php index 2342fb2466..f027ff8ae4 100644 --- a/tests/PHPStan/Analyser/data/bug-2899.php +++ b/tests/PHPStan/Analyser/data/bug-2899.php @@ -9,8 +9,8 @@ class Foo public function doFoo(string $s, $mixed) { - assertType('string&numeric', date('Y')); - assertType('string', date('Y.m.d')); + assertType('numeric-string', date('Y')); + assertType('non-empty-string', date('Y.m.d')); assertType('string', date($s)); assertType('string', date($mixed)); } diff --git a/tests/PHPStan/Analyser/data/bug-3009.php b/tests/PHPStan/Analyser/data/bug-3009.php index de9f96ca4c..5d9b2cdd12 100644 --- a/tests/PHPStan/Analyser/data/bug-3009.php +++ b/tests/PHPStan/Analyser/data/bug-3009.php @@ -11,18 +11,18 @@ public function createRedirectRequest(string $redirectUri): ?string { $redirectUrlParts = parse_url($redirectUri); if (false === is_array($redirectUrlParts) || true === array_key_exists('host', $redirectUrlParts)) { - assertType('array(?\'scheme\' => string, ?\'host\' => string, ?\'port\' => int, ?\'user\' => string, ?\'pass\' => string, ?\'path\' => string, ?\'query\' => string, ?\'fragment\' => string)|false', $redirectUrlParts); + assertType('array{scheme?: string, host: string, port?: int, user?: string, pass?: string, path?: string, query?: string, fragment?: string}|false', $redirectUrlParts); return null; } - assertType('array(?\'scheme\' => string, ?\'host\' => string, ?\'port\' => int, ?\'user\' => string, ?\'pass\' => string, ?\'path\' => string, ?\'query\' => string, ?\'fragment\' => string)', $redirectUrlParts); + assertType('array{scheme?: string, host?: string, port?: int, 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, ?\'user\' => string, ?\'pass\' => string, ?\'path\' => string, \'query\' => string, ?\'fragment\' => string)', $redirectUrlParts); + assertType('array{scheme?: string, host?: string, port?: int, user?: string, pass?: string, path?: string, query: string, fragment?: string}', $redirectUrlParts); $redirectServer['QUERY_STRING'] = $redirectUrlParts['query']; } - assertType('array(?\'scheme\' => string, ?\'host\' => string, ?\'port\' => int, ?\'user\' => string, ?\'pass\' => string, ?\'path\' => string, ?\'query\' => string, ?\'fragment\' => string)', $redirectUrlParts); + assertType('array{scheme?: string, host?: string, port?: int, user?: string, pass?: string, path?: string, query?: string, fragment?: string}', $redirectUrlParts); return 'foo'; } diff --git a/tests/PHPStan/Analyser/data/bug-3044.php b/tests/PHPStan/Analyser/data/bug-3044.php new file mode 100644 index 0000000000..fc3d6e53ff --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-3044.php @@ -0,0 +1,42 @@ +> + */ + private function getIntArrayFloatArray(): array + { + return [ + 0 => [1.1, 2.2, 3.3], + 1 => [1.1, 2.2, 3.3], + 2 => [1.1, 2.2, 3.3], + ]; + } + + /** + * @return array> + */ + public function invalidType(): void + { + $X = $this->getIntArrayFloatArray(); + $L = $this->getIntArrayFloatArray(); + + $n = 3; + $m = 3; + + for ($k = 0; $k < $m; ++$k) { + for ($j = 0; $j < $n; ++$j) { + for ($i = 0; $i < $k; ++$i) { + $X[$k][$j] -= $X[$i][$j] * $L[$k][$i]; + } + } + } + + assertType('array>', $X); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-3126.php b/tests/PHPStan/Analyser/data/bug-3126.php new file mode 100644 index 0000000000..9f675a4239 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-3126.php @@ -0,0 +1,37 @@ + $input + */ + function failure(array $input): void { + $results = []; + + foreach ($input as $keyOne => $layerOne) { + assertType('bool', isset($results[$keyOne]['name'])); + if(isset($results[$keyOne]['name']) === false) { + $results[$keyOne]['name'] = $layerOne; + } + } + } + + /** + * @param array $input + */ + function no_failure(array $input): void { + $results = []; + + foreach ($input as $keyOne => $layerOne) { + if(isset($results[$keyOne]) === false) { + $results[$keyOne] = $layerOne; + } + } + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-3133.php b/tests/PHPStan/Analyser/data/bug-3133.php index ea003461fa..98eb5841f0 100644 --- a/tests/PHPStan/Analyser/data/bug-3133.php +++ b/tests/PHPStan/Analyser/data/bug-3133.php @@ -17,7 +17,7 @@ public function doFoo($arg): void return; } - assertType('string&numeric', $arg); + assertType('numeric-string', $arg); } /** @@ -26,7 +26,7 @@ public function doFoo($arg): void public function doBar($arg): void { if (\is_numeric($arg)) { - assertType('float|int|(string&numeric)', $arg); + assertType('float|int|numeric-string', $arg); } } @@ -39,8 +39,8 @@ public function doBaz( string $numericString ) { - assertType('float|int|(string&numeric)', $numeric); - assertType('string&numeric', $numericString); + assertType('float|int|numeric-string', $numeric); + assertType('numeric-string', $numericString); } /** @@ -52,7 +52,7 @@ public function doLorem( { $a = []; $a[$numericString] = 'foo'; - assertType('array&nonEmpty', $a); + assertType('non-empty-array', $a); } } diff --git a/tests/PHPStan/Analyser/data/bug-3266.php b/tests/PHPStan/Analyser/data/bug-3266.php index 9e1d2a3ebe..bee0c7e6f8 100644 --- a/tests/PHPStan/Analyser/data/bug-3266.php +++ b/tests/PHPStan/Analyser/data/bug-3266.php @@ -21,7 +21,7 @@ public function iteratorToArray($iterator) assertType('TKey of (int|string) (method Bug3266\Foo::iteratorToArray(), argument)', $key); assertType('TValue (method Bug3266\Foo::iteratorToArray(), argument)', $value); $array[$key] = $value; - assertType('array&nonEmpty', $array); + assertType('non-empty-array', $array); } assertType('array', $array); diff --git a/tests/PHPStan/Analyser/data/bug-3269.php b/tests/PHPStan/Analyser/data/bug-3269.php index b3cfdbea68..f7f5a2bce6 100644 --- a/tests/PHPStan/Analyser/data/bug-3269.php +++ b/tests/PHPStan/Analyser/data/bug-3269.php @@ -20,10 +20,10 @@ public static function bar(array $intervalGroups): void } } - assertType('array string, \'operator\' => string, \'side\' => \'end\'|\'start\')>', $borders); + assertType('array', $borders); foreach ($borders as $border) { - assertType('array(\'version\' => string, \'operator\' => string, \'side\' => \'end\'|\'start\')', $border); + assertType('array{version: string, operator: string, side: \'end\'|\'start\'}', $border); assertType('\'end\'|\'start\'', $border['side']); } } diff --git a/tests/PHPStan/Analyser/data/bug-3276.php b/tests/PHPStan/Analyser/data/bug-3276.php index dccb79c5fc..ece368c9b1 100644 --- a/tests/PHPStan/Analyser/data/bug-3276.php +++ b/tests/PHPStan/Analyser/data/bug-3276.php @@ -13,7 +13,7 @@ class Foo public function doFoo(array $settings): void { $settings['name'] ??= 'unknown'; - assertType('array(\'name\' => string)', $settings); + assertType('array{name: string}', $settings); } /** @@ -22,7 +22,7 @@ public function doFoo(array $settings): void public function doBar(array $settings): void { $settings['name'] = 'unknown'; - assertType('array(\'name\' => \'unknown\')', $settings); + assertType('array{name: \'unknown\'}', $settings); } } diff --git a/tests/PHPStan/Analyser/data/bug-3351.php b/tests/PHPStan/Analyser/data/bug-3351.php index 83198c9c1d..2de71cddc2 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 $c = $this->combine($a, $b); assertType('array|false', $c); - assertType('array(\'a\' => 1, \'b\' => 2, \'c\' => 3)', array_combine($a, $b)); + assertType('array{a: 1, b: 2, c: 3}', array_combine($a, $b)); } /** diff --git a/tests/PHPStan/Analyser/data/bug-3379.php b/tests/PHPStan/Analyser/data/bug-3379.php index e6b0cb5df4..53d82890e3 100644 --- a/tests/PHPStan/Analyser/data/bug-3379.php +++ b/tests/PHPStan/Analyser/data/bug-3379.php @@ -2,6 +2,8 @@ namespace Bug3379; +use function PHPStan\Testing\assertType; + class Foo { @@ -11,4 +13,5 @@ class Foo function () { echo Foo::URL; + assertType('mixed', Foo::URL); }; diff --git a/tests/PHPStan/Analyser/data/bug-3382.php b/tests/PHPStan/Analyser/data/bug-3382.php index 3623d2a9cf..973b489f4c 100644 --- a/tests/PHPStan/Analyser/data/bug-3382.php +++ b/tests/PHPStan/Analyser/data/bug-3382.php @@ -5,5 +5,5 @@ use function PHPStan\Testing\assertType; if (ini_get('auto_prepend_file')) { - assertType('string', ini_get('auto_prepend_file')); + assertType('non-empty-string', ini_get('auto_prepend_file')); } diff --git a/tests/PHPStan/Analyser/data/bug-3558.php b/tests/PHPStan/Analyser/data/bug-3558.php index 49dd89b06c..b473c961bb 100644 --- a/tests/PHPStan/Analyser/data/bug-3558.php +++ b/tests/PHPStan/Analyser/data/bug-3558.php @@ -14,7 +14,7 @@ function (): void { } if(count($idGroups) > 0){ - assertType('array(array(1, 2), array(1, 2), array(1, 2))', $idGroups); + assertType('array{array{1, 2}, array{1, 2}, array{1, 2}}', $idGroups); } }; @@ -28,6 +28,6 @@ function (): void { } if(count($idGroups) > 1){ - assertType('array(0 => 1, ?1 => array(1, 2), ?2 => array(1, 2), ?3 => array(1, 2))', $idGroups); + assertType('array{0: 1, 1?: array{1, 2}, 2?: array{1, 2}, 3?: array{1, 2}}', $idGroups); } }; diff --git a/tests/PHPStan/Analyser/data/bug-3858.php b/tests/PHPStan/Analyser/data/bug-3858.php new file mode 100644 index 0000000000..341e64a9f6 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-3858.php @@ -0,0 +1,25 @@ +&nonEmpty', $lengths); + assertType('non-empty-array', $lengths); } public static function getInt(): int diff --git a/tests/PHPStan/Analyser/data/bug-3961-php8.php b/tests/PHPStan/Analyser/data/bug-3961-php8.php index 2fb86e13e4..93b19e3857 100644 --- a/tests/PHPStan/Analyser/data/bug-3961-php8.php +++ b/tests/PHPStan/Analyser/data/bug-3961-php8.php @@ -9,13 +9,13 @@ class Foo public function doFoo(string $v, string $d, $m): void { - assertType('array&nonEmpty', explode('.', $v)); + assertType('non-empty-array', explode('.', $v)); assertType('*NEVER*', explode('', $v)); assertType('array', explode('.', $v, -2)); - assertType('array&nonEmpty', explode('.', $v, 0)); - assertType('array&nonEmpty', explode('.', $v, 1)); - assertType('array', explode($d, $v)); - assertType('array', explode($m, $v)); + assertType('non-empty-array', explode('.', $v, 0)); + assertType('non-empty-array', explode('.', $v, 1)); + assertType('non-empty-array', explode($d, $v)); + assertType('non-empty-array', explode($m, $v)); } } diff --git a/tests/PHPStan/Analyser/data/bug-3961.php b/tests/PHPStan/Analyser/data/bug-3961.php index 5666786e33..35a1a2ba11 100644 --- a/tests/PHPStan/Analyser/data/bug-3961.php +++ b/tests/PHPStan/Analyser/data/bug-3961.php @@ -9,13 +9,13 @@ class Foo public function doFoo(string $v, string $d, $m): void { - assertType('array&nonEmpty', explode('.', $v)); + assertType('non-empty-array', explode('.', $v)); assertType('false', explode('', $v)); assertType('array', explode('.', $v, -2)); - assertType('array&nonEmpty', explode('.', $v, 0)); - assertType('array&nonEmpty', explode('.', $v, 1)); - assertType('array|false', explode($d, $v)); - assertType('(array|false)', explode($m, $v)); + assertType('non-empty-array', explode('.', $v, 0)); + assertType('non-empty-array', explode('.', $v, 1)); + assertType('non-empty-array|false', explode($d, $v)); + assertType('(non-empty-array|false)', explode($m, $v)); } } diff --git a/tests/PHPStan/Analyser/data/bug-3981.php b/tests/PHPStan/Analyser/data/bug-3981.php new file mode 100644 index 0000000000..2a1929cf1a --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-3981.php @@ -0,0 +1,25 @@ +', $a); $a[] = 2; - assertType('array&nonEmpty', $a); + assertType('non-empty-array', $a); unset($a[0]); assertType('array', $a); @@ -27,7 +27,7 @@ public function doBar(array $a): void { assertType('array', $a); $a[1] = 2; - assertType('array&nonEmpty', $a); + assertType('non-empty-array', $a); unset($a[1]); assertType('array', $a); diff --git a/tests/PHPStan/Analyser/data/bug-4097.php b/tests/PHPStan/Analyser/data/bug-4097.php index cd27b2f0f7..c18ad8ec14 100644 --- a/tests/PHPStan/Analyser/data/bug-4097.php +++ b/tests/PHPStan/Analyser/data/bug-4097.php @@ -11,6 +11,18 @@ class Bar {} */ class SnapshotRepository { + + /** @var T[] */ + private $entities; + + /** + * @param T[] $entities + */ + public function __construct(array $entities) + { + $this->entities = $entities; + } + /** * @return \Traversable */ @@ -18,7 +30,7 @@ public function findAllSnapshots(): \Traversable { yield from \array_map( \Closure::fromCallable([$this, 'buildSnapshot']), - [] + $this->entities ); } diff --git a/tests/PHPStan/Analyser/data/bug-4099.php b/tests/PHPStan/Analyser/data/bug-4099.php index 571dfb3476..2e49179969 100644 --- a/tests/PHPStan/Analyser/data/bug-4099.php +++ b/tests/PHPStan/Analyser/data/bug-4099.php @@ -13,7 +13,7 @@ class Foo */ function arrayHint(array $arr): void { - assertType('array(\'key\' => array(\'inner\' => mixed))', $arr); + assertType('array{key: array{inner: mixed}}', $arr); assertNativeType('array', $arr); if (!array_key_exists('key', $arr)) { @@ -21,21 +21,21 @@ function arrayHint(array $arr): void assertNativeType('array', $arr); throw new \Exception('no key "key" found.'); } - assertType('array(\'key\' => array(\'inner\' => mixed))', $arr); + assertType('array{key: array{inner: mixed}}', $arr); assertNativeType('array&hasOffset(\'key\')', $arr); - assertType('array(\'inner\' => mixed)', $arr['key']); + assertType('array{inner: mixed}', $arr['key']); assertNativeType('mixed', $arr['key']); if (!array_key_exists('inner', $arr['key'])) { - assertType('array(\'key\' => *NEVER*)', $arr); + assertType('array{key: *NEVER*}', $arr); //assertNativeType('array(\'key\' => mixed)', $arr); assertType('*NEVER*', $arr['key']); //assertNativeType('mixed', $arr['key']); throw new \Exception('need key.inner'); } - assertType('array(\'key\' => array(\'inner\' => mixed))', $arr); - assertNativeType('array(\'key\' => array(\'inner\' => mixed))', $arr); + assertType('array{key: array{inner: mixed}}', $arr); + assertNativeType('array{key: array{inner: mixed}}', $arr); } } diff --git a/tests/PHPStan/Analyser/data/bug-4207.php b/tests/PHPStan/Analyser/data/bug-4207.php index e8a1de5f02..0a7ae998d4 100644 --- a/tests/PHPStan/Analyser/data/bug-4207.php +++ b/tests/PHPStan/Analyser/data/bug-4207.php @@ -5,5 +5,6 @@ use function PHPStan\Testing\assertType; function (): void { - assertType('array&nonEmpty', range(1, 10000)); + assertType('non-empty-array>', range(1, 10000)); + assertType('non-empty-array>', range(10000, 1)); }; diff --git a/tests/PHPStan/Analyser/data/bug-4213.php b/tests/PHPStan/Analyser/data/bug-4213.php index bc9742be9a..538bacbb79 100644 --- a/tests/PHPStan/Analyser/data/bug-4213.php +++ b/tests/PHPStan/Analyser/data/bug-4213.php @@ -37,7 +37,7 @@ public function setEnumsWithoutSplat(array $enums): void { function (): void { assertType('Bug4213\Enum', Enum::get('test')); - assertType('array(Bug4213\Enum)', array_map([Enum::class, 'get'], ['test'])); + assertType('array{Bug4213\\Enum}', array_map([Enum::class, 'get'], ['test'])); }; diff --git a/tests/PHPStan/Analyser/data/bug-4267.php b/tests/PHPStan/Analyser/data/bug-4267.php index be533bafcd..c93256827b 100644 --- a/tests/PHPStan/Analyser/data/bug-4267.php +++ b/tests/PHPStan/Analyser/data/bug-4267.php @@ -10,6 +10,7 @@ class Model1 implements \IteratorAggregate { + #[\ReturnTypeWillChange] public function getIterator(): iterable { throw new \Exception('not implemented'); @@ -33,6 +34,7 @@ class Model2 implements \IteratorAggregate /** * @return iterable */ + #[\ReturnTypeWillChange] public function getIterator(): iterable { throw new \Exception('not implemented'); diff --git a/tests/PHPStan/Analyser/data/bug-4357.php b/tests/PHPStan/Analyser/data/bug-4357.php new file mode 100644 index 0000000000..d8e36019c8 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4357.php @@ -0,0 +1,26 @@ + */ + private $arr = null; + + public function test(): void { + if ($this->arr === null) { + return; + } + + assertType('array', $this->arr); + + unset($this->arr['hello']); + + assertType('array', $this->arr); + + if (count($this->arr) === 0) { + $this->arr = null; + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-4398.php b/tests/PHPStan/Analyser/data/bug-4398.php index 23bab3eaa2..ee0c2a2565 100644 --- a/tests/PHPStan/Analyser/data/bug-4398.php +++ b/tests/PHPStan/Analyser/data/bug-4398.php @@ -10,9 +10,9 @@ function (array $meters): void { throw new \Exception('NO_METERS_FOUND'); } - assertType('array&nonEmpty', $meters); + assertType('non-empty-array', $meters); assertType('array', array_reverse()); - assertType('array&nonEmpty', array_reverse($meters)); - assertType('array&nonEmpty', array_keys($meters)); - assertType('array&nonEmpty', array_values($meters)); + assertType('non-empty-array', array_reverse($meters)); + assertType('non-empty-array', array_keys($meters)); + assertType('non-empty-array', array_values($meters)); }; diff --git a/tests/PHPStan/Analyser/data/bug-4434.php b/tests/PHPStan/Analyser/data/bug-4434.php index b60b751ed1..7e0b97d7fb 100644 --- a/tests/PHPStan/Analyser/data/bug-4434.php +++ b/tests/PHPStan/Analyser/data/bug-4434.php @@ -10,14 +10,14 @@ class HelloWorld public function testSendEmailToLog(): void { foreach ([1] as $emailFile) { - assertType('int', PHP_MAJOR_VERSION); - assertType('int', \PHP_MAJOR_VERSION); + assertType('int<5, max>', PHP_MAJOR_VERSION); + assertType('int<5, max>', \PHP_MAJOR_VERSION); if (PHP_MAJOR_VERSION === 7) { assertType('int', PHP_MAJOR_VERSION); assertType('int', \PHP_MAJOR_VERSION); } else { - assertType('int|int<8, max>', PHP_MAJOR_VERSION); - assertType('int|int<8, max>', \PHP_MAJOR_VERSION); + assertType('int<5, 6>|int<8, max>', PHP_MAJOR_VERSION); + assertType('int<5, 6>|int<8, max>', \PHP_MAJOR_VERSION); } } } @@ -28,14 +28,14 @@ class HelloWorld2 public function testSendEmailToLog(): void { foreach ([1] as $emailFile) { - assertType('int', PHP_MAJOR_VERSION); - assertType('int', \PHP_MAJOR_VERSION); + assertType('int<5, max>', PHP_MAJOR_VERSION); + assertType('int<5, max>', \PHP_MAJOR_VERSION); if (PHP_MAJOR_VERSION === 100) { assertType('int', PHP_MAJOR_VERSION); assertType('int', \PHP_MAJOR_VERSION); } else { - assertType('int|int<101, max>', PHP_MAJOR_VERSION); - assertType('int|int<101, max>', \PHP_MAJOR_VERSION); + assertType('int<5, 99>|int<101, max>', PHP_MAJOR_VERSION); + assertType('int<5, 99>|int<101, max>', \PHP_MAJOR_VERSION); } } } diff --git a/tests/PHPStan/Analyser/data/bug-4499.php b/tests/PHPStan/Analyser/data/bug-4499.php new file mode 100644 index 0000000000..046da4efa1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4499.php @@ -0,0 +1,19 @@ + $things */ + function thing(array $things) : void{ + switch(count($things)){ + case 1: + assertType('non-empty-array', $things); + assertType('int', array_shift($things)); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-4504.php b/tests/PHPStan/Analyser/data/bug-4504.php index 50ba802671..f70d3c9567 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('array{}|Iterator', $models); } } diff --git a/tests/PHPStan/Analyser/data/bug-4558.php b/tests/PHPStan/Analyser/data/bug-4558.php index 89b250cc0b..a40e33581a 100644 --- a/tests/PHPStan/Analyser/data/bug-4558.php +++ b/tests/PHPStan/Analyser/data/bug-4558.php @@ -15,7 +15,7 @@ class HelloWorld public function sayHello(): ?DateTime { while (count($this->suggestions) > 0) { - assertType('array&nonEmpty', $this->suggestions); + assertType('non-empty-array', $this->suggestions); assertType('int<1, max>', count($this->suggestions)); $try = array_shift($this->suggestions); @@ -31,7 +31,7 @@ public function sayHello(): ?DateTime // we might be out of suggested days, so load some more if (count($this->suggestions) === 0) { - assertType('array()', $this->suggestions); + assertType('array{}', $this->suggestions); assertType('0', count($this->suggestions)); $this->createSuggestions(); } diff --git a/tests/PHPStan/Analyser/data/bug-4579.php b/tests/PHPStan/Analyser/data/bug-4579.php index bcaa6bb812..4b7c095153 100644 --- a/tests/PHPStan/Analyser/data/bug-4579.php +++ b/tests/PHPStan/Analyser/data/bug-4579.php @@ -6,7 +6,7 @@ function (string $class): void { $foo = new $class(); - assertType('mixed~string', $foo); + assertType('object', $foo); if (method_exists($foo, 'doFoo')) { assertType('object&hasMethod(doFoo)', $foo); } diff --git a/tests/PHPStan/Analyser/data/bug-4586.php b/tests/PHPStan/Analyser/data/bug-4586.php new file mode 100644 index 0000000000..82291ec0df --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4586.php @@ -0,0 +1,13 @@ + int)', $result); + assertType('array{a: int}', $result); return $result; }, $results); - assertType('array int)>', $type); + assertType('array', $type); } public function b(): void @@ -25,13 +25,13 @@ public function b(): void $results = []; $type = array_map(static function (array $result): array { - assertType('array(\'a\' => int)', $result); + assertType('array{a: int}', $result); $result['a'] = (string) $result['a']; - assertType('array(\'a\' => string&numeric)', $result); + assertType('array{a: numeric-string}', $result); return $result; }, $results); - assertType('array string&numeric)>', $type); + assertType('array', $type); } } diff --git a/tests/PHPStan/Analyser/data/bug-4592.php b/tests/PHPStan/Analyser/data/bug-4592.php new file mode 100644 index 0000000000..5d66bf93c5 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4592.php @@ -0,0 +1,30 @@ + + */ + private $contacts1 = []; + + /** + * @var array{names: array, emails: array} + */ + private $contacts2 = ['names' => [], 'emails' => []]; + + public function sayHello1(string $id): void + { + $name = $this->contacts1[$id]['name'] ?? null; + assertType('string|null', $name); + } + + public function sayHello2(string $id): void + { + $name = $this->contacts2['names'][$id] ?? null; + assertType('string|null', $name); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-4602.php b/tests/PHPStan/Analyser/data/bug-4602.php new file mode 100644 index 0000000000..2e843364e5 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4602.php @@ -0,0 +1,21 @@ +', $limit - 1); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-4606.php b/tests/PHPStan/Analyser/data/bug-4606.php index d2d6a3ab45..1cf9cf4a32 100644 --- a/tests/PHPStan/Analyser/data/bug-4606.php +++ b/tests/PHPStan/Analyser/data/bug-4606.php @@ -11,7 +11,7 @@ */ assertType(Foo::class, $this); -assertType('array', $assigned); +assertType('array', $assigned); /** @@ -20,4 +20,4 @@ */ $foo = doFoo(); -assertType('array(stdClass, int)', $foo); +assertType('array{stdClass, int}', $foo); diff --git a/tests/PHPStan/Analyser/data/bug-4650.php b/tests/PHPStan/Analyser/data/bug-4650.php index f378375869..f51b260c26 100644 --- a/tests/PHPStan/Analyser/data/bug-4650.php +++ b/tests/PHPStan/Analyser/data/bug-4650.php @@ -12,11 +12,11 @@ class Foo * @phpstan-param non-empty-array $idx */ function doFoo(array $idx): void { - assertType('array&nonEmpty', $idx); + assertType('non-empty-array', $idx); assertNativeType('array', $idx); - assertType('array()', []); - assertNativeType('array()', []); + assertType('array{}', []); + assertNativeType('array{}', []); assertType('false', $idx === []); assertNativeType('bool', $idx === []); diff --git a/tests/PHPStan/Analyser/data/bug-4700.php b/tests/PHPStan/Analyser/data/bug-4700.php index bf73f23a09..e50cce6f7c 100644 --- a/tests/PHPStan/Analyser/data/bug-4700.php +++ b/tests/PHPStan/Analyser/data/bug-4700.php @@ -19,10 +19,10 @@ function(array $array, int $count): void { if (isset($array['e'])) $a[] = $array['e']; if (count($a) >= $count) { assertType('1|2|3|4|5', count($a)); - assertType('array(0 => mixed~null, ?1 => mixed~null, ?2 => mixed~null, ?3 => mixed~null, ?4 => mixed~null)', $a); + assertType('array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); } else { assertType('0|1|2|3|4|5', count($a)); - assertType('array()|array(0 => mixed~null, ?1 => mixed~null, ?2 => mixed~null, ?3 => mixed~null, ?4 => mixed~null)', $a); + assertType('array{}|array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); } }; @@ -41,9 +41,9 @@ function(array $array, int $count): void { if (isset($array['e'])) $a[] = $array['e']; if (count($a) > $count) { assertType('2|3|4|5', count($a)); - assertType('array(0 => mixed~null, ?1 => mixed~null, ?2 => mixed~null, ?3 => mixed~null, ?4 => mixed~null)', $a); + assertType('array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); } else { assertType('0|1|2|3|4|5', count($a)); - assertType('array()|array(0 => mixed~null, ?1 => mixed~null, ?2 => mixed~null, ?3 => mixed~null, ?4 => mixed~null)', $a); + assertType('array{}|array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); } }; diff --git a/tests/PHPStan/Analyser/data/bug-4711.php b/tests/PHPStan/Analyser/data/bug-4711.php new file mode 100644 index 0000000000..8d25957907 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4711.php @@ -0,0 +1,19 @@ +', explode($string, '')); + assertType('non-empty-array', explode($string[0], '')); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-4715.php b/tests/PHPStan/Analyser/data/bug-4715.php index 14ea1ff1f2..d51a97b3e4 100644 --- a/tests/PHPStan/Analyser/data/bug-4715.php +++ b/tests/PHPStan/Analyser/data/bug-4715.php @@ -19,7 +19,7 @@ class ArrayCollection implements Collection /** * {@inheritDoc} */ - public function getIterator() + public function getIterator(): \Traversable { return new \ArrayIterator([]); } diff --git a/tests/PHPStan/Analyser/data/bug-4734.php b/tests/PHPStan/Analyser/data/bug-4734.php index 6b231e0f9b..8f7d6af714 100644 --- a/tests/PHPStan/Analyser/data/bug-4734.php +++ b/tests/PHPStan/Analyser/data/bug-4734.php @@ -15,6 +15,22 @@ class Foo * @var bool */ private $httpMethodParameterOverride2 = true; + + /** + * @return bool + */ + public static function isHttpMethodParameterOverride(): bool + { + return self::$httpMethodParameterOverride; + } + + /** + * @return bool + */ + public function isHttpMethodParameterOverride2(): bool + { + return $this->httpMethodParameterOverride2; + } } class Bar diff --git a/tests/PHPStan/Analyser/data/bug-4741.php b/tests/PHPStan/Analyser/data/bug-4741.php new file mode 100644 index 0000000000..fd2191fe83 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4741.php @@ -0,0 +1,15 @@ + + */ + private $nodes; + + /** + * @phpstan-param array $nodes + */ + public function __construct(array $nodes) + { + $this->nodes = $nodes; + } + + public function splice(int $offset, int $length): void + { + $newNodes = array_splice($this->nodes, $offset, $length); + + assertType('array', $this->nodes); + assertType('array', $newNodes); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-4814.php b/tests/PHPStan/Analyser/data/bug-4814.php index 4e8607a793..2fcaf231f4 100644 --- a/tests/PHPStan/Analyser/data/bug-4814.php +++ b/tests/PHPStan/Analyser/data/bug-4814.php @@ -33,7 +33,7 @@ public function doFoo() $decodedResponseBody = json_decode($body, true, 512, JSON_THROW_ON_ERROR); } catch (\Throwable $exception) { assertType('string|null', $body); - assertType('array()', $decodedResponseBody); + assertType('array{}', $decodedResponseBody); } } diff --git a/tests/PHPStan/Analyser/data/bug-4843.php b/tests/PHPStan/Analyser/data/bug-4843.php new file mode 100644 index 0000000000..ab021b55af --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4843.php @@ -0,0 +1,17 @@ +', $this->depth + ($isRoot ? 0 : 1)); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-4887.php b/tests/PHPStan/Analyser/data/bug-4887.php new file mode 100644 index 0000000000..ab0a549013 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4887.php @@ -0,0 +1,18 @@ + $_REQUEST, + '$_COOKIE' => $_COOKIE, + '$_SERVER' => $_SERVER, + '$GLOBALS' => $GLOBALS, + '$SESSION' => isset($_SESSION) ? $_SESSION : NULL]; + +foreach ($foo as $data) +{ + assertType('bool', is_array($data)); +} diff --git a/tests/PHPStan/Analyser/data/bug-4896.php b/tests/PHPStan/Analyser/data/bug-4896.php new file mode 100644 index 0000000000..c8345a364e --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4896.php @@ -0,0 +1,38 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug4896; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + public function doFoo(\DateTime|\DateInterval $command): void + { + switch ($command::class) { + case \DateTime::class: + assertType(\DateTime::class, $command); + break; + case \DateInterval::class: + assertType(\DateInterval::class, $command); + break; + } + + } + +} + +class Bar +{ + + public function doFoo(\DateTime|\DateInterval $command): void + { + match ($command::class) { + \DateTime::class => assertType(\DateTime::class, $command), + \DateInterval::class => assertType(\DateInterval::class, $command), + }; + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-4902-php8.php b/tests/PHPStan/Analyser/data/bug-4902-php8.php new file mode 100644 index 0000000000..016ac2586f --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4902-php8.php @@ -0,0 +1,53 @@ += 7.4 + +namespace Bug4902; + +use function PHPStan\Testing\assertType; + +/** + * @template T-wrapper + */ +class Wrapper { + /** @var T-wrapper */ + public $value; + + /** + * @param T-wrapper $value + */ + public function __construct($value) { + $this->value = $value; + } + + /** + * @template T-unwrap + * @param Wrapper $wrapper + * @return T-unwrap + */ + function unwrap(Wrapper $wrapper) { + return $wrapper->value; + } + + /** + * @template T-wrap + * @param T-wrap $value + * + * @return Wrapper + */ + function wrap($value): Wrapper + { + return new Wrapper($value); + } + + + /** + * @template T-all + * @param Wrapper ...$wrappers + */ + function unwrapAllAndWrapAgain(Wrapper ...$wrappers): void { + assertType('array', array_map(function (Wrapper $item) { + return $this->unwrap($item); + }, $wrappers)); + assertType('array', array_map(fn (Wrapper $item) => $this->unwrap($item), $wrappers)); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-4903.php b/tests/PHPStan/Analyser/data/bug-4903.php new file mode 100644 index 0000000000..4b6b1fd459 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4903.php @@ -0,0 +1,27 @@ +importFile)) { + return 1; + } + assertType('true', \file_exists($this->importFile)); + $this->importFile = '/b'; + assertType('bool', \file_exists($this->importFile)); + + if (\file_exists($this->importFile)) { + echo 'test'; + } + + return \file_exists($this->importFile) ? 0 : 1; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-5017.php b/tests/PHPStan/Analyser/data/bug-5017.php new file mode 100644 index 0000000000..fa1abc5b46 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5017.php @@ -0,0 +1,56 @@ +', $items); + $batch = array_splice($items, 0, 2); + assertType('array<0|1|2|3|4, 0|1|2|3|4>', $items); + assertType('array<0|1|2|3|4, 0|1|2|3|4>', $batch); + } + } + + /** + * @param int[] $items + */ + public function doBar($items) + { + while ($items) { + assertType('non-empty-array', $items); + $batch = array_splice($items, 0, 2); + assertType('array', $items); + assertType('array', $batch); + } + } + + public function doBar2() + { + $items = [0, 1, 2, 3, 4]; + assertType('array{0, 1, 2, 3, 4}', $items); + $batch = array_splice($items, 0, 2); + assertType('array<0|1|2|3|4, 0|1|2|3|4>', $items); + assertType('array<0|1|2|3|4, 0|1|2|3|4>', $batch); + } + + /** + * @param int[] $ints + * @param string[] $strings + */ + public function doBar3(array $ints, array $strings) + { + $removed = array_splice($ints, 0, 2, $strings); + assertType('array', $removed); + assertType('array', $ints); + assertType('array', $strings); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-5072.php b/tests/PHPStan/Analyser/data/bug-5072.php new file mode 100644 index 0000000000..25f273346b --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5072.php @@ -0,0 +1,29 @@ + $params + */ + public function incorrect(array $params): void + { + $page = isset($params['page']) ? intval($params['page']) : 1; + assertType('int<1, max>', max(1, $page)); + } + + public function incorrectWithConstant(): void + { + assertType('2147483647|9223372036854775807', max(1, PHP_INT_MAX)); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-5129.php b/tests/PHPStan/Analyser/data/bug-5129.php new file mode 100644 index 0000000000..925594b386 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5129.php @@ -0,0 +1,63 @@ +foo = ''; + assertType('0', strlen($this->foo)); + if (strlen($this->foo) > 0) { + return; + } + + assertType('0', strlen($this->foo)); + + $this->foo = 'x'; + assertType('1', strlen($this->foo)); + if (strlen($this->foo) > 0) { + return; + } + + assertType('0', strlen($this->foo)); + + $this->foo = $s; + assertType('int<0, max>', strlen($this->foo)); + } + + public function sayHello2(string $s): void + { + $this->foo = ''; + if (!$this->isFoo($this->foo)) { + return; + } + + assertType('true', $this->isFoo($this->foo)); + + $this->foo = 'x'; + assertType('bool', $this->isFoo($this->foo)); + if (!$this->isFoo($this->foo)) { + return; + } + assertType('true', $this->isFoo($this->foo)); + + $this->foo = $s; + assertType('bool', $this->isFoo($this->foo)); + if (!$this->isFoo($this->foo)) { + return; + } + assertType('true', $this->isFoo($this->foo)); + } + + public function isFoo(string $s): bool + { + return strlen($s) % 3; + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-5219.php b/tests/PHPStan/Analyser/data/bug-5219.php index 2ff5b1c47e..91ce62bb02 100644 --- a/tests/PHPStan/Analyser/data/bug-5219.php +++ b/tests/PHPStan/Analyser/data/bug-5219.php @@ -7,12 +7,12 @@ class HelloWorld { - protected function foo(string $message): void + protected function foo(string $message, string $x): void { - $header = sprintf('%s-%s', '', implode('-', ['x'])); + $header = sprintf('%s-%s', '', implode('-', [$x])); - assertType('string', $header); - assertType('array&nonEmpty', [$header => $message]); + assertType('non-empty-string', $header); + assertType('non-empty-array', [$header => $message]); } protected function bar(string $message): void @@ -20,6 +20,6 @@ protected function bar(string $message): void $header = sprintf('%s-%s', '', ''); assertType('\'-\'', $header); - assertType('array(\'-\' => string)', [$header => $message]); + assertType('array{-: string}', [$header => $message]); } } diff --git a/tests/PHPStan/Analyser/data/bug-5231.php b/tests/PHPStan/Analyser/data/bug-5231.php new file mode 100644 index 0000000000..f42aebe125 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5231.php @@ -0,0 +1,78 @@ +collection as $item) { + if ((string)$item === $name) { + return $item; + } + } + + return null; + } + + // must be exists! + public function existsByKey(string $name): bool + { + return $this->findByName($name) !== null; + } + + public function getSorted(callable $comparator): self + { + $sortedCollection = $this->collection; + usort($sortedCollection, $comparator); + + $filtered = array_values($sortedCollection); + + return new static(...$filtered); + } + + public function rewind(): void + { + reset($this->collection); + } + + public function current() + { + return current($this->collection); + } + + /** + * @return bool|float|int|string|null + */ + public function key() + { + return key($this->collection); + } + + /** + * @return mixed|void + */ + public function next() + { + return next($this->collection); + } + + public function valid(): bool + { + return isset($this->collection[key($this->collection)]); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-5231_2.php b/tests/PHPStan/Analyser/data/bug-5231_2.php new file mode 100644 index 0000000000..07e65e5b26 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5231_2.php @@ -0,0 +1,78 @@ +collection as $item) { + if ((string)$item === $name) { + return $item; + } + } + + return null; + } + + // must be exists! + public function existsByKey(string $name): bool + { + return $this->findByName($name) !== null; + } + + public function getSorted(callable $comparator): self + { + $sortedCollection = $this->collection; + usort($sortedCollection, $comparator); + + $filtered = array_values($sortedCollection); + + return new static(...$filtered); + } + + public function rewind(): void + { + reset($this->collection); + } + + public function current() + { + return current($this->collection); + } + + /** + * @return bool|float|int|string|null + */ + public function key() + { + return key($this->collection); + } + + /** + * @return mixed|void + */ + public function next() + { + return next($this->collection); + } + + public function valid(): bool + { + return isset($this->collection[key($this->collection)]); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-5259.php b/tests/PHPStan/Analyser/data/bug-5259.php new file mode 100644 index 0000000000..35e9bca126 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5259.php @@ -0,0 +1,30 @@ + $arr + */ +function foo(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('array', $arrSpread); +} + +/** + * @param list $arr + */ +function foo2(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('non-empty-array', $arrSpread); +} + +/** + * @param non-empty-list $arr + */ +function foo3(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('non-empty-array', $arrSpread); +} + +/** + * @param non-empty-array $arr + */ +function foo3(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('non-empty-array', $arrSpread); +} + +/** + * @param non-empty-array $arr + */ +function foo4(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('non-empty-array', $arrSpread); +} + +/** + * @param array{foo: 17, bar: 19} $arr + */ +function bar(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('array{foo: 17, bar: 19}', $arrSpread); +} diff --git a/tests/PHPStan/Analyser/data/bug-5287.php b/tests/PHPStan/Analyser/data/bug-5287.php new file mode 100644 index 0000000000..3bcb5eb4fb --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5287.php @@ -0,0 +1,59 @@ + $arr + */ +function foo(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('array', $arrSpread); +} + +/** + * @param list $arr + */ +function foo2(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('non-empty-array', $arrSpread); +} + +/** + * @param non-empty-list $arr + */ +function foo3(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('non-empty-array', $arrSpread); +} + +/** + * @param non-empty-array $arr + */ +function foo3(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('non-empty-array', $arrSpread); +} + +/** + * @param non-empty-array $arr + */ +function foo4(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('non-empty-array', $arrSpread); +} + +/** + * @param array{foo: 17, bar: 19} $arr + */ +function bar(array $arr): void +{ + $arrSpread = [...$arr]; + assertType('array{17, 19}', $arrSpread); +} diff --git a/tests/PHPStan/Analyser/data/bug-5293.php b/tests/PHPStan/Analyser/data/bug-5293.php new file mode 100644 index 0000000000..40d8ca8cc4 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5293.php @@ -0,0 +1,58 @@ + 'foo', + 2 => 'foo', + 3 => 'bar', + ]; + $names = ['foo', 'bar', 'baz']; + $array = ['foo' => [], 'bar' => [], 'baz' => []]; + + foreach ($map as $value => $name) { + $array[$name][] = $value; + } + + + foreach ($array as $name => $elements) { + assertType('bool', count($elements) > 0); + assertType('array', $elements); + } +}; diff --git a/tests/PHPStan/Analyser/data/bug-5322.php b/tests/PHPStan/Analyser/data/bug-5322.php new file mode 100644 index 0000000000..71965701da --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5322.php @@ -0,0 +1,35 @@ +produceIntOrNull(); + } + + assertType('int', $int); + } + + function doBar() + { + $int = $this->produceIntOrNull(); + while (!is_int($int)) { + $int = $this->produceIntOrNull(); + } + + assertType('int', $int); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-5328.php b/tests/PHPStan/Analyser/data/bug-5328.php new file mode 100644 index 0000000000..5bb7c7d301 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5328.php @@ -0,0 +1,25 @@ +produceIntOrNull(); + for ($i = 0; $i < 5 && !is_int($int) ; $i++) { + $int = $this->produceIntOrNull(); + } + + assertType('int|null', $int); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-5458.php b/tests/PHPStan/Analyser/data/bug-5458.php new file mode 100644 index 0000000000..4dd1da8451 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5458.php @@ -0,0 +1,23 @@ +prop2 = 5; + + if ($this->isBroken()){ + return; + } + + assertType('false', $this->isBroken()); + assertType('5', $this->prop2); + + $this->damage = min($this->damage + $amount, 5); + + assertType('bool', $this->isBroken()); + assertType('5', $this->prop2); + } + + public function applyDamage2(int $amount): void + { + $this->prop2 = 5; + + if ($this->isBroken()){ + return; + } + + assertType('false', $this->isBroken()); + assertType('5', $this->prop2); + + $this->array['foo'] = min($this->damage + $amount, 5); + + assertType('bool', $this->isBroken()); + assertType('5', $this->prop2); + } + + protected function onBroken(): void + { + + } + + public function isBroken(): bool{ + return $this->damage >= 5; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-5527.php b/tests/PHPStan/Analyser/data/bug-5527.php new file mode 100644 index 0000000000..076e6626fc --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5527.php @@ -0,0 +1,20 @@ +run()); +}; diff --git a/tests/PHPStan/Analyser/data/bug-5530.php b/tests/PHPStan/Analyser/data/bug-5530.php new file mode 100644 index 0000000000..6cf8dd8f9a --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5530.php @@ -0,0 +1,29 @@ + 5]; + } + + $b = []; + + if (rand(0,1) === 0) { + $b = ['b' => 6]; + } + + assertType('array{}|array{b?: 6, a?: 5}', $a + $b); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-5615.php b/tests/PHPStan/Analyser/data/bug-5615.php new file mode 100644 index 0000000000..5c8f8027fb --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5615.php @@ -0,0 +1,29 @@ +getRandomSuit()); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-5639.php b/tests/PHPStan/Analyser/data/bug-5639.php new file mode 100644 index 0000000000..c0eeed3370 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5639.php @@ -0,0 +1,22 @@ +message, $this->file, $this->line); + return $result; + } + } +} +else +{ + class Foo extends \Error { + function __toString(): string { + $result = \sprintf("%s\n\nin %s on line %s", $this->message, $this->file, $this->line); + return $result; + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-5657.php b/tests/PHPStan/Analyser/data/bug-5657.php new file mode 100644 index 0000000000..7cb6fa91f0 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5657.php @@ -0,0 +1,13 @@ + $in + */ + function has(array $in): void + { + assertType('bool', in_array('test', $in, true)); + } + + /** + * @param array $in + */ + function has2(array $in): void + { + assertType('bool', in_array('test', $in, true)); + } + + /** + * @param non-empty-array $in + */ + function has3(array $in): void + { + assertType('bool', in_array('test', $in, true)); + } + + + /** + * @param non-empty-array $in + */ + function has4(array $in): void + { + assertType('true', in_array('test', $in, true)); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-5675.php b/tests/PHPStan/Analyser/data/bug-5675.php new file mode 100644 index 0000000000..bde301d4ec --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5675.php @@ -0,0 +1,55 @@ +): void)|array|Bar $column + */ + public function foo($column): void + { + // ... + } + + public function bar() + { + /** @var Hello */ + $a = new Hello; + + $a->foo(function (Hello $h) : void { + assertType('Bug5675\Hello', $h); + }); + } +} + +/** + * @template T + */ +class Hello2 +{ + /** + * @param (\Closure(static): void)|array|Bar $column + */ + public function foo($column): void + { + // ... + } + + public function bar() + { + /** @var Hello2 */ + $a = new Hello2; + + $a->foo(function (Hello2 $h) : void { + \PHPStan\Testing\assertType('Bug5675\Hello2', $h); + }); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-5698-php7.php b/tests/PHPStan/Analyser/data/bug-5698-php7.php new file mode 100644 index 0000000000..10d2ceb899 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5698-php7.php @@ -0,0 +1,16 @@ +', $foo); + assertNativeType('array', $foo); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-5698-php8.php b/tests/PHPStan/Analyser/data/bug-5698-php8.php new file mode 100644 index 0000000000..91a541351d --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5698-php8.php @@ -0,0 +1,16 @@ +', $foo); + assertNativeType('array', $foo); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-5759.php b/tests/PHPStan/Analyser/data/bug-5759.php new file mode 100644 index 0000000000..e3511e869b --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5759.php @@ -0,0 +1,39 @@ + $fields */ + function strict(array $fields): void + { + assertType('bool', in_array(ITF::FIELD_A, $fields, true)); + } + + + /** @param array $fields */ + function loose(array $fields): void + { + assertType('bool', in_array(ITF::FIELD_A, $fields, false)); + } + + function another(): void + { + /** @var array<'source'|'dist'> $arr */ + $arr = ['source']; + + assertType('bool', in_array('dist', $arr, true)); + assertType('bool', in_array('dist', $arr)); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-5817.php b/tests/PHPStan/Analyser/data/bug-5817.php new file mode 100644 index 0000000000..ff7a7ca765 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5817.php @@ -0,0 +1,127 @@ + + * @implements Iterator + */ +class MyContainer implements + ArrayAccess, + Countable, + Iterator, + JsonSerializable +{ + /** @var array */ + protected $items = []; + + public function add(DateTimeInterface $item, int $offset = null): self + { + $this->offsetSet($offset, $item); + return $this; + } + + public function count(): int + { + return count($this->items); + } + + /** @return DateTimeInterface|false */ + #[\ReturnTypeWillChange] + public function current() + { + return current($this->items); + } + + /** @return DateTimeInterface|false */ + #[\ReturnTypeWillChange] + public function next() + { + return next($this->items); + } + + /** @return int|null */ + public function key(): ?int + { + return key($this->items); + } + + public function valid(): bool + { + return $this->key() !== null; + } + + /** @return DateTimeInterface|false */ + #[\ReturnTypeWillChange] + public function rewind() + { + return reset($this->items); + } + + /** @param mixed $offset */ + public function offsetExists($offset): bool + { + return isset($this->items[$offset]); + } + + /** @param mixed $offset */ + public function offsetGet($offset): ?DateTimeInterface + { + return $this->items[$offset] ?? null; + } + + /** + * @param mixed $offset + * @param mixed $value + */ + public function offsetSet($offset, $value): void + { + assert($value instanceof DateTimeInterface); + if ($offset === null) { // append + $this->items[] = $value; + } else { + $this->items[$offset] = $value; + } + } + + /** @param mixed $offset */ + public function offsetUnset($offset): void + { + unset($this->items[$offset]); + } + + /** @return DateTimeInterface[] */ + public function jsonSerialize(): array + { + return $this->items; + } +} + +class Foo +{ + + public function doFoo() + { + $container = (new MyContainer())->add(new \DateTimeImmutable()); + + foreach ($container as $k => $item) { + assertType('int', $k); + assertType(DateTimeInterface::class, $item); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-5843.php b/tests/PHPStan/Analyser/data/bug-5843.php new file mode 100644 index 0000000000..d9793fe2c8 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5843.php @@ -0,0 +1,36 @@ += 8.0 + +namespace Bug5843; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + function doFoo(object $object): void + { + assertType('class-string', $object::class); + switch ($object::class) { + case \DateTime::class: + assertType(\DateTime::class, $object); + break; + case \Throwable::class: + assertType(\Throwable::class, $object); + break; + } + } + +} + +class Bar +{ + + function doFoo(object $object): void + { + match ($object::class) { + \DateTime::class => assertType(\DateTime::class, $object), + \Throwable::class => assertType(\Throwable::class, $object), + }; + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-5951.php b/tests/PHPStan/Analyser/data/bug-5951.php new file mode 100644 index 0000000000..a774868adc --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5951.php @@ -0,0 +1,31 @@ += 8.0 + +namespace Bug5951; + +#[\Attribute] +class Route +{ + + /** @param string[] $methods */ + public function __construct(public string $path, public string $name, public array $methods) + { + + } + +} + +class Response +{ + +} + +final class SomeController +{ + public const ROUTE_INDEX = 'some_index'; + + #[Route('/some', name: self::ROUTE_INDEX, methods: ['GET'])] + public function index(): Response + { + return new Response(); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-5992.php b/tests/PHPStan/Analyser/data/bug-5992.php new file mode 100644 index 0000000000..6c1ab9c021 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5992.php @@ -0,0 +1,24 @@ +getCode()); + assertType('(int|string)', $t->getCode()); + assertType('(int|string)', (new \RuntimeException())->getCode()); + assertType('int', (new \LogicException())->getCode()); + assertType('(int|string)', (new \PDOException())->getCode()); + assertType('int', (new MyException())->getCode()); + assertType('(int|string)', (new SubPDOException())->getCode()); + assertType('1|2|3', (new ExceptionWithMethodTag())->getCode()); + } + + /** + * @param \PDOException|MyException $exception + * @return void + */ + public function doBar($exception): void + { + assertType('(int|string)', $exception->getCode()); + } + +} + +class MyException extends \Exception +{ + +} + +class SubPDOException extends \PDOException +{ + +} + +/** + * @method 1|2|3 getCode() + */ +class ExceptionWithMethodTag extends \Exception +{ + +} diff --git a/tests/PHPStan/Analyser/data/bug-6070.php b/tests/PHPStan/Analyser/data/bug-6070.php new file mode 100644 index 0000000000..ac1355eee0 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6070.php @@ -0,0 +1,23 @@ +>', $nonEmptyArray); + + return $nonEmptyArray; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6108.php b/tests/PHPStan/Analyser/data/bug-6108.php new file mode 100644 index 0000000000..e19760df07 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6108.php @@ -0,0 +1,38 @@ + [1, 2], + 'b' => [3, 4, 5], + 'c' => true, + ]; + } + + function doBar() + { + $x = $this->doFoo(); + $test = ['a' => true, 'b' => false]; + foreach ($test as $key => $value) { + if ($value) { + assertType('\'a\'|\'b\'', $key); // could be just 'a' + assertType('array', $x[$key]); + } + } + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-6114.php b/tests/PHPStan/Analyser/data/bug-6114.php new file mode 100644 index 0000000000..73b7c4f507 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6114.php @@ -0,0 +1,45 @@ += 8.0 + +namespace Bug6114; + +/** + * @template T + */ +interface Foo { + /** + * @return T + */ + public function bar(): mixed; +} + +class HelloWorld +{ + /** + * @template T + * @param T $value + * @return Foo + */ + public function sayHello(mixed $value): Foo + { + return new + /** + * @template U + * @implements Foo + */ class($value) implements Foo { + /** @var U */ + private mixed $value; + + /** + * @param U $value + */ + public function __construct(mixed $value) { + $this->value = $value; + } + + public function bar(): mixed + { + return $this->value; + } + }; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6174.php b/tests/PHPStan/Analyser/data/bug-6174.php new file mode 100644 index 0000000000..08dcc48747 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6174.php @@ -0,0 +1,21 @@ +returnValue() ?? self::DEFAULT_VALUE); + assertType('-1|int<1, max>', $tempValue === -1 || $tempValue > 0 ? $tempValue : self::DEFAULT_VALUE); + } + + public function returnValue(): ?string + { + return null; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6212.php b/tests/PHPStan/Analyser/data/bug-6212.php new file mode 100644 index 0000000000..a9aa125bde --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6212.php @@ -0,0 +1,13 @@ +pgsqlGetNotify(\PDO::FETCH_ASSOC); diff --git a/tests/PHPStan/Analyser/data/bug-6293.php b/tests/PHPStan/Analyser/data/bug-6293.php new file mode 100644 index 0000000000..0a5c8548be --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6293.php @@ -0,0 +1,23 @@ +withPhpDoc(null); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6300.php b/tests/PHPStan/Analyser/data/bug-6300.php new file mode 100644 index 0000000000..c9244c6cec --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6300.php @@ -0,0 +1,26 @@ +get(); + echo $b->fooProp; +}; + diff --git a/tests/PHPStan/Analyser/data/bug-6305.php b/tests/PHPStan/Analyser/data/bug-6305.php new file mode 100644 index 0000000000..5cf5d353b4 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6305.php @@ -0,0 +1,19 @@ += 8.0 + +declare(strict_types=1); + +namespace Bug6308; + +use function PHPStan\Testing\assertType; + +class BaseFinderStatic +{ + static public function find(): false|static + { + return false; + } +} + +final class UnionStaticStrict extends BaseFinderStatic +{ + public function something() + { + assertType('Bug6308\UnionStaticStrict|false', $this->find()); + } +} + +class UnionStaticStrict2 extends BaseFinderStatic +{ + public function something() + { + assertType('static(Bug6308\UnionStaticStrict2)|false', $this->find()); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6329.php b/tests/PHPStan/Analyser/data/bug-6329.php new file mode 100644 index 0000000000..c6b49e87e4 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6329.php @@ -0,0 +1,185 @@ + 0 || null === $a) { + assertType('non-empty-string|null', $a); + } + + if (null === $a || is_string($a) && strlen($a) > 0) { + assertType('non-empty-string|null', $a); + } +} + + +/** + * @param mixed $a + */ +function int1($a): void +{ + if (is_int($a) && 0 !== $a || null === $a) { + assertType('int|int<1, max>|null', $a); + } + + if (0 !== $a && is_int($a) || null === $a) { + assertType('int|int<1, max>|null', $a); + } + + if (null === $a || is_int($a) && 0 !== $a) { + assertType('int|int<1, max>|null', $a); + } + + if (null === $a || 0 !== $a && is_int($a)) { + assertType('int|int<1, max>|null', $a); + } +} + +/** + * @param mixed $a + */ +function int2($a): void +{ + if (is_int($a) && $a > 0 || null === $a) { + assertType('int<1, max>|null', $a); + } + + if (null === $a || is_int($a) && $a > 0) { + assertType('int<1, max>|null', $a); + } +} + + +/** + * @param mixed $a + */ +function true($a): void +{ + if (is_bool($a) && false !== $a || null === $a) { + assertType('true|null', $a); + } + + if (false !== $a && is_bool($a) || null === $a) { + assertType('true|null', $a); + } + + if (null === $a || is_bool($a) && false !== $a) { + assertType('true|null', $a); + } + + if (null === $a || false !== $a && is_bool($a)) { + assertType('true|null', $a); + } +} + +/** + * @param mixed $a + */ +function nonEmptyArray1($a): void +{ + if (is_array($a) && [] !== $a || null === $a) { + assertType('non-empty-array|null', $a); + } + + if ([] !== $a && is_array($a) || null === $a) { + assertType('non-empty-array|null', $a); + } + + if (null === $a || is_array($a) && [] !== $a) { + assertType('non-empty-array|null', $a); + } + + if (null === $a || [] !== $a && is_array($a)) { + assertType('non-empty-array|null', $a); + } +} + +/** + * @param mixed $a + */ +function nonEmptyArray2($a): void +{ + if (is_array($a) && count($a) > 0 || null === $a) { + assertType('non-empty-array|null', $a); + } + + if (null === $a || is_array($a) && count($a) > 0) { + assertType('non-empty-array|null', $a); + } +} + +/** + * @param mixed $a + * @param mixed $b + * @param mixed $c + */ +function inverse($a, $b, $c): void +{ + if ((!is_string($a) || '' === $a) && null !== $a) { + } else { + assertType('non-empty-string|null', $a); + } + + if ((!is_int($b) || $b <= 0) && null !== $b) { + } else { + assertType('int<1, max>|null', $b); + } + + if (null !== $c && (!is_array($c) || count($c) <= 0)) { + } else { + assertType('non-empty-array|null', $c); + } +} + +/** + * @param mixed $a + * @param mixed $b + * @param mixed $c + * @param mixed $d + */ +function combinations($a, $b, $c, $d): void +{ + if (is_string($a) && '' !== $a || is_int($a) && $a > 0 || null === $a) { + assertType('int<1, max>|non-empty-string|null', $a); + } + if ((!is_string($b) || '' === $b) && (!is_int($b) || $b <= 0) && null !== $b) { + } else { + assertType('int<1, max>|non-empty-string|null', $b); + } + + if (is_array($c) && $c === array_filter($c, 'is_string', ARRAY_FILTER_USE_KEY) || null === $c) { + assertType('array|null', $c); + } + if ((!is_array($d) || $d !== array_filter($d, 'is_string', ARRAY_FILTER_USE_KEY)) && null !== $d) { + } else { + assertType('array|null', $d); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6375.php b/tests/PHPStan/Analyser/data/bug-6375.php new file mode 100644 index 0000000000..80f9d9b1bf --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6375.php @@ -0,0 +1,19 @@ + $i + * @return void + */ + public function sayHello($i): void + { + $a = []; + $a[$i] = 5; + assertType('non-empty-array, 5>', $a); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6399.php b/tests/PHPStan/Analyser/data/bug-6399.php new file mode 100644 index 0000000000..bc3eb70c59 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6399.php @@ -0,0 +1,60 @@ +>|null + */ + private static $threadLocalStorage = null; + + final public function __destruct(){ + assertType('ArrayObject>|null', self::$threadLocalStorage); + if(self::$threadLocalStorage !== null){ + assertType('ArrayObject>', self::$threadLocalStorage); + if (isset(self::$threadLocalStorage[$h = spl_object_id($this)])) { + assertType('ArrayObject>&hasOffset(int)', self::$threadLocalStorage); + unset(self::$threadLocalStorage[$h]); + assertType('ArrayObject>', self::$threadLocalStorage); + if(self::$threadLocalStorage->count() === 0){ + self::$threadLocalStorage = null; + } + } + } + } + + public function doFoo(): void + { + if(self::$threadLocalStorage === null) { + return; + } + + assertType('ArrayObject>', self::$threadLocalStorage); + if (isset(self::$threadLocalStorage[1])) { + assertType('ArrayObject>&hasOffset(1)', self::$threadLocalStorage); + } else { + assertType('ArrayObject>', self::$threadLocalStorage); + } + + assertType('ArrayObject>', self::$threadLocalStorage); + if (isset(self::$threadLocalStorage[1]) && isset(self::$threadLocalStorage[2])) { + assertType('ArrayObject>&hasOffset(1)&hasOffset(2)', self::$threadLocalStorage); + unset(self::$threadLocalStorage[2]); + assertType('ArrayObject>&hasOffset(1)', self::$threadLocalStorage); + } + } + + /** + * @param non-empty-array $a + * @return void + */ + public function doBar(array $a): void + { + assertType('non-empty-array', $a); + unset($a[1]); + assertType('array', $a); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-6404.php b/tests/PHPStan/Analyser/data/bug-6404.php new file mode 100644 index 0000000000..422c6e84f1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6404.php @@ -0,0 +1,85 @@ + + */ + private $someMap = []; + + public function build(): void + { + foreach (self::FOOS as $fooClass) { + if (is_a($fooClass, Foo::class, true)) { + assertType("'Bug6404\\\\Foo'", $fooClass); + assertType('int', $fooClass::getCode()); + $this->someMap[$fooClass::getCode()] = true; + } + } + } + + /** + * @param object[] $objects + * @return void + */ + public function build2(array $objects): void + { + foreach ($objects as $fooClass) { + if (is_a($fooClass, Foo::class)) { + assertType(Foo::class, $fooClass); + assertType('int', $fooClass::getCode()); + $this->someMap[$fooClass::getCode()] = true; + } + } + } + + /** + * @param mixed[] $mixeds + * @return void + */ + public function build3(array $mixeds): void + { + foreach ($mixeds as $fooClass) { + if (is_a($fooClass, Foo::class, true)) { + assertType('Bug6404\\Foo|class-string', $fooClass); + assertType('int', $fooClass::getCode()); + $this->someMap[$fooClass::getCode()] = true; + } + } + } + + /** + * @param class-string $classString + * @return void + */ + public function doBar(string $classString): void + { + assertType("class-string<" . Foo::class . ">", $classString); + assertType('int', $classString::getCode()); + } + + /** + * @return array + */ + public function getAll(): array + { + return $this->someMap; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6433.php b/tests/PHPStan/Analyser/data/bug-6433.php new file mode 100644 index 0000000000..9a31de7ef1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6433.php @@ -0,0 +1,21 @@ += 8.1 + +namespace Bug6433; + +use Ds\Set; +use function PHPStan\Testing\assertType; + +enum E: string { + case A = 'A'; + case B = 'B'; +} + +class Foo +{ + + function x(): void { + assertType('Ds\Set', new Set([E::A, E::B])); + } + +} + diff --git a/tests/PHPStan/Analyser/data/bug-6439.php b/tests/PHPStan/Analyser/data/bug-6439.php new file mode 100644 index 0000000000..9ebb1c2ac6 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6439.php @@ -0,0 +1,82 @@ + $value) { + if ($value % 2 === 0) { + unset($items[$key]); + } + } + + assertType('bool',sizeof($items) > 0); +} diff --git a/tests/PHPStan/Analyser/data/bug-6497.php b/tests/PHPStan/Analyser/data/bug-6497.php new file mode 100644 index 0000000000..23d08e4e6b --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6497.php @@ -0,0 +1,18 @@ + */ + $array = [ + ['foo' => 'baz', 'bar' => 3], + ]; + + $array2 = array_column($array, null, 'foo'); + + assertType('array', $array2); +} diff --git a/tests/PHPStan/Analyser/data/bug-6500.php b/tests/PHPStan/Analyser/data/bug-6500.php new file mode 100644 index 0000000000..d6a1be8fc1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6500.php @@ -0,0 +1,14 @@ += 8.0 + +namespace Bug6505; + +use function PHPStan\Testing\assertType; + +/** @template T */ +interface Type +{ + /** + * @param T $val + * @return T + */ + public function validate($val); +} + +/** + * @template T + * @implements Type> + */ +final class ClassStringType implements Type +{ + /** @param class-string $classString */ + public function __construct(public string $classString) + { + } + + public function validate($val) { + return $val; + } +} + +/** + * @implements Type> + */ +final class StdClassType implements Type +{ + public function validate($val) { + return $val; + } +} + + +/** + * @template T + * @implements Type + */ +final class TypeCollection implements Type +{ + /** @param Type $type */ + public function __construct(public Type $type) + { + } + public function validate($val) { + return $val; + } +} + +class Foo +{ + + public function doFoo() + { + $c = new TypeCollection(new ClassStringType(\stdClass::class)); + assertType('array>', $c->validate([\stdClass::class])); + $c2 = new TypeCollection(new StdClassType()); + assertType('array>', $c2->validate([\stdClass::class])); + } + + /** + * @template T + * @param T $t + * @return T + */ + function unbounded($t) { + return $t; + } + + /** + * @template T of string + * @param T $t + * @return T + */ + function bounded1($t) { + return $t; + } + + /** + * @template T of object|class-string + * @param T $t + * @return T + */ + function bounded2($t) { + return $t; + } + + /** @param class-string<\stdClass> $p */ + function test($p): void { + assertType('class-string', $this->unbounded($p)); + assertType('class-string', $this->bounded1($p)); + assertType('class-string', $this->bounded2($p)); + } + +} + +/** + * @template TKey of array-key + * @template TValue + */ +class Collection +{ + /** + * @var array + */ + protected array $items; + + /** + * Create a new collection. + * + * @param array|null $items + * @return void + */ + public function __construct(?array $items = []) + { + $this->items = $items ?? []; + } +} + +class Example +{ + /** @var array> */ + private array $factories = []; + + public function getFactories(): void + { + assertType('Bug6505\Collection>', new Collection($this->factories)); + } +} + diff --git a/tests/PHPStan/Analyser/data/bug-6566-types.php b/tests/PHPStan/Analyser/data/bug-6566-types.php new file mode 100644 index 0000000000..199f9d2a03 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6566-types.php @@ -0,0 +1,49 @@ += 8.0 + +namespace Bug6566Types; + +use function PHPStan\Testing\assertType; + +class A { + public string $name; +} + +class B { + public string $name; +} + +class C { + +} + +/** + * @template T of A|B|C + */ +abstract class HelloWorld +{ + public function sayHelloBug(): void + { + $object = $this->getObject(); + assertType('T of Bug6566Types\A|Bug6566Types\B|Bug6566Types\C (class Bug6566Types\HelloWorld, argument)', $object); + if ($object instanceof C) { + assertType('T of Bug6566Types\C (class Bug6566Types\HelloWorld, argument)', $object); + return; + } + assertType('T of Bug6566Types\A|Bug6566Types\B (class Bug6566Types\HelloWorld, argument)', $object); + if ($object instanceof B) { + assertType('T of Bug6566Types\B (class Bug6566Types\HelloWorld, argument)', $object); + return; + } + assertType('T of Bug6566Types\A (class Bug6566Types\HelloWorld, argument)', $object); + if ($object instanceof A) { + assertType('T of Bug6566Types\A (class Bug6566Types\HelloWorld, argument)', $object); + return; + } + assertType('*NEVER*', $object); + } + + /** + * @return T + */ + abstract protected function getObject(): A|B|C; +} diff --git a/tests/PHPStan/Analyser/data/bug-6584.php b/tests/PHPStan/Analyser/data/bug-6584.php new file mode 100644 index 0000000000..c989a35199 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6584.php @@ -0,0 +1,44 @@ +same($int)); + assertType('int', $this->sameWithDefault($int)); + + assertType('int|null', $this->same($intOrNull)); + assertType('int|null', $this->sameWithDefault($intOrNull)); + + assertType('null', $this->same(null)); + assertType('null', $this->sameWithDefault(null)); + assertType('null', $this->sameWithDefault()); + } + + + /** + * @template T + * @param T $t + * @return T + */ + function same($t) { + assertType('T (method Bug6584\Foo::same(), argument)', $t); + return $t; + } + + /** + * @template T + * @param T $t + * @return T + */ + function sameWithDefault($t = null) { + assertType('T (method Bug6584\Foo::sameWithDefault(), argument)', $t); + return $t; + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-6591.php b/tests/PHPStan/Analyser/data/bug-6591.php new file mode 100644 index 0000000000..01ddac64cc --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6591.php @@ -0,0 +1,74 @@ + + */ + public function extract(object $object): array; +} + +interface EntityInterface { + public const IDENTITY = 'identity'; + public const CREATED = 'created'; + public function getIdentity(): string; + public function getCreated(): \DateTimeImmutable; +} +interface UpdatableInterface extends EntityInterface { + public const UPDATED = 'updated'; + public function getUpdated(): \DateTimeImmutable; + public function setUpdated(\DateTimeImmutable $updated): void; +} +interface EnableableInterface extends UpdatableInterface { + public const ENABLED = 'enabled'; + public function isEnabled(): bool; + public function setEnabled(bool $enabled): void; +} + + +/** + * @template T of EntityInterface + */ +class DoctrineEntityHydrator implements HydratorInterface +{ + /** @param T $object */ + public function extract(object $object): array + { + $data = [ + EntityInterface::IDENTITY => $object->getIdentity(), + EntityInterface::CREATED => $object->getCreated()->format('c'), + ]; + assertType('T of Bug6591\EntityInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object); + if ($object instanceof UpdatableInterface) { + assertType('Bug6591\UpdatableInterface&T of Bug6591\EntityInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object); + $data[UpdatableInterface::UPDATED] = $object->getUpdated()->format('c'); + } else { + assertType('T of Bug6591\EntityInterface~Bug6591\UpdatableInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object); + } + + assertType('T of Bug6591\EntityInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object); + + if ($object instanceof EnableableInterface) { + assertType('Bug6591\EnableableInterface&T of Bug6591\EntityInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object); + $data[EnableableInterface::ENABLED] = $object->isEnabled(); + } else { + assertType('T of Bug6591\EntityInterface~Bug6591\EnableableInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object); + } + + assertType('T of Bug6591\EntityInterface (class Bug6591\DoctrineEntityHydrator, argument)', $object); + + return [...$data, ...$this->performExtraction($object)]; + } + + /** + * @param T $entity + * @return array + */ + public function performExtraction(EntityInterface $entity): array + { + return []; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6624.php b/tests/PHPStan/Analyser/data/bug-6624.php new file mode 100644 index 0000000000..bbdf3b9395 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6624.php @@ -0,0 +1,31 @@ + 17) { + assertType('int<18, max>', $a); + } else { + assertType('int', $a); + } + + if ($b > 17 || $b === null) { + assertType('int<18, max>|null', $b); + } else { + assertType('int', $b); + } + + if ($c < 17) { + assertType('int', $c); + } else { + assertType('int<17, max>', $c); + } + + if ($d < 17 || $d === null) { + assertType('int|null', $d); + } else { + assertType('int<17, max>', $d); + } + + if ($e >= 17 && $e <= 19 || $e === null) { + assertType('int<17, 19>|null', $e); + } else { + assertType('int|int<20, max>', $e); + } + + if ($f < 17 || $f > 19 || $f === null) { + assertType('int|int<20, max>|null', $f); + } else { + assertType('int<17, 19>', $f); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6681.php b/tests/PHPStan/Analyser/data/bug-6681.php new file mode 100644 index 0000000000..836f247d91 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6681.php @@ -0,0 +1,18 @@ + []]; +} + + + +$apiCacheMap = new class extends ApiCacheMap { + protected const CACHE_MAP = [ + 1 => ApiCacheMap::CACHE_MAP[self::DEFAULT_CACHE_TTL], + ]; +}; diff --git a/tests/PHPStan/Analyser/data/bug-6682.php b/tests/PHPStan/Analyser/data/bug-6682.php new file mode 100644 index 0000000000..cdb2738ceb --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6682.php @@ -0,0 +1,19 @@ +|null>> $data + */ + public function __construct(array $data) + { + $x = array_column($data, null, 'type'); + assertType('array|string|null>>', $x); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6687.php b/tests/PHPStan/Analyser/data/bug-6687.php new file mode 100644 index 0000000000..d3de13e67c --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6687.php @@ -0,0 +1,35 @@ +', $a); + } + } + + function bar(string $a): void + { + if ($a === 'FOO' || is_subclass_of($a, 'FOO')) { + assertType('class-string', $a); + } + } + + function baz(string $a): void + { + if ($a === BAZ || is_subclass_of($a, BAZ)) { + assertType('class-string', $a); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-6695.php b/tests/PHPStan/Analyser/data/bug-6695.php new file mode 100644 index 0000000000..094c142ce7 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6695.php @@ -0,0 +1,58 @@ += 8.1 + +namespace Bug6695; + +use function PHPStan\Testing\assertType; + +enum Foo: int +{ + case BAR = 1; + case BAZ = 2; + + public function toCollection(): void + { + assertType('Bug6695\Collection', $this->collect(self::cases())); + } + + /** + * Create a collection from the given value. + * + * @template TKey of array-key + * @template TValue + * + * @param iterable $value + * @return Collection + */ + function collect($value): Collection + { + return new Collection($value); + } + +} + +/** + * @template TKey of array-key + * @template TValue + * + */ +class Collection +{ + + /** + * The items contained in the collection. + * + * @var iterable + */ + protected $items = []; + + /** + * Create a new collection. + * + * @param iterable $items + * @return void + */ + public function __construct($items = []) + { + $this->items = $items; + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6696.php b/tests/PHPStan/Analyser/data/bug-6696.php new file mode 100644 index 0000000000..6e4dd96666 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6696.php @@ -0,0 +1,20 @@ + + */ + public function getClasses(): iterable; +} + +class Y +{ + /** @var X */ + public $x; + + /** + * @template T of object + * + * @param class-string $type + * @return iterable> + */ + public function findImplementations(string $type): iterable + { + foreach ($this->x->getClasses() as $class) { + if (is_subclass_of($class, $type)) { + assertType('class-string', $class); + yield $class; + } + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6699.php b/tests/PHPStan/Analyser/data/bug-6699.php new file mode 100644 index 0000000000..6e5bad05c4 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6699.php @@ -0,0 +1,30 @@ +value = $value; + } + + /** + * @param class-string<\Exception> $exceptionClass + * @return void + */ + public function doFoo(string $exceptionClass) + { + assertType('class-string', (new Foo($exceptionClass))->value); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6704.php b/tests/PHPStan/Analyser/data/bug-6704.php new file mode 100644 index 0000000000..f342fed93a --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6704.php @@ -0,0 +1,22 @@ +|class-string $a + * @param DateTimeImmutable|stdClass $b + */ +function foo(string $a, object $b): void +{ + if (!is_a($a, stdClass::class, true)) { + assertType('class-string', $a); + } + + if (!is_a($b, stdClass::class)) { + assertType('DateTimeImmutable', $b); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6715.php b/tests/PHPStan/Analyser/data/bug-6715.php new file mode 100644 index 0000000000..1916bea3be --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6715.php @@ -0,0 +1,49 @@ + true, + ])); + + assertType('array{}', array_filter([ + 'test' => false, + ])); + + assertType('array{test?: 1}', array_filter([ + 'test' => rand(0, 1), + ])); + + assertType('array{test?: true}', array_filter([ + 'test' => $this->bool, + ])); + } + + function test2(): void + { + assertType('\'1\'', implode(', ', array_filter([ + 'test' => true, + ]))); + + assertType('\'\'', implode(', ', array_filter([ + 'test' => false, + ]))); + + assertType('\'\'|\'1\'', implode(', ', array_filter([ + 'test' => rand(0, 1), + ]))); + + assertType('\'\'|\'1\'', implode(', ', array_filter([ + 'test' => $this->bool, + ]))); + } +} diff --git a/tests/PHPStan/Analyser/data/bug-6740-a.php b/tests/PHPStan/Analyser/data/bug-6740-a.php new file mode 100644 index 0000000000..b244f217f6 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6740-a.php @@ -0,0 +1,53 @@ +StdClassSetup(get_class()); + } +} + +class A +{ + /** @var string[] */ + + private $classList = []; + + /** + * @returns $this + */ + + public function __construct() + { + } + + /** + * Apply all the standard configuration needs for a sub-class + * + * @param string $baseClass + */ + + public function StdClassSetup($baseClass): void + { + $this->classList[] = $baseClass; + } + + /** + * @return string[] + */ + + public function GetClassList() + { + return $this->classList; + } +} + +class Box extends A +{ + use BlockTemplate; +} diff --git a/tests/PHPStan/Analyser/data/bug-6740-b.php b/tests/PHPStan/Analyser/data/bug-6740-b.php new file mode 100644 index 0000000000..f4a9b44b6b --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6740-b.php @@ -0,0 +1,8 @@ += 8.0 + +namespace Bug6790; + +use function PHPStan\Testing\assertType; + +/** + * @template T + */ +class Repository +{ + /** + * @param array $items + */ + public function __construct(private array $items) {} + + /** + * @return ?T + */ + public function find(string $id) + { + return $this->items[$id] ?? null; + } +} + +/** + * @template T + */ +class Repository2 +{ + /** + * @param array $items + */ + public function __construct(private array $items) {} + + /** + * @return T|null + */ + public function find(string $id) + { + return $this->items[$id] ?? null; + } +} + +class Foo +{ + + /** + * @param Repository $r + * @return void + */ + public function doFoo(Repository $r): void + { + assertType('string|null', $r->find('foo')); + } + + /** + * @param Repository2 $r + * @return void + */ + public function doFoo2(Repository2 $r): void + { + assertType('string|null', $r->find('foo')); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-empty-array.php b/tests/PHPStan/Analyser/data/bug-empty-array.php index c50df67513..91a6b8bb00 100644 --- a/tests/PHPStan/Analyser/data/bug-empty-array.php +++ b/tests/PHPStan/Analyser/data/bug-empty-array.php @@ -14,9 +14,9 @@ public function doFoo(): void { assertType('array', $this->comments); $this->comments = []; - assertType('array()', $this->comments); + assertType('array{}', $this->comments); if ($this->comments === []) { - assertType('array()', $this->comments); + assertType('array{}', $this->comments); return; } else { assertType('*NEVER*', $this->comments); @@ -29,9 +29,9 @@ public function doBar(): void { assertType('array', $this->comments); $this->comments = []; - assertType('array()', $this->comments); + assertType('array{}', $this->comments); if ([] === $this->comments) { - assertType('array()', $this->comments); + assertType('array{}', $this->comments); return; } else { assertType('*NEVER*', $this->comments); diff --git a/tests/PHPStan/Analyser/data/bug-pr-339.php b/tests/PHPStan/Analyser/data/bug-pr-339.php index 01e3822e96..768efbc147 100644 --- a/tests/PHPStan/Analyser/data/bug-pr-339.php +++ b/tests/PHPStan/Analyser/data/bug-pr-339.php @@ -17,17 +17,17 @@ assertType('mixed', $a); assertType('mixed', $c); if ($a) { - assertType("mixed~0|0.0|''|'0'|array()|false|null", $a); + assertType("mixed~0|0.0|''|'0'|array{}|false|null", $a); assertType('mixed', $c); assertVariableCertainty(TrinaryLogic::createYes(), $a); } if ($c) { assertType('mixed', $a); - assertType("mixed~0|0.0|''|'0'|array()|false|null", $c); + assertType("mixed~0|0.0|''|'0'|array{}|false|null", $c); assertVariableCertainty(TrinaryLogic::createYes(), $c); } } else { - assertType("0|0.0|''|'0'|array()|false|null", $a); - assertType("0|0.0|''|'0'|array()|false|null", $c); + assertType("0|0.0|''|'0'|array{}|false|null", $a); + assertType("0|0.0|''|'0'|array{}|false|null", $c); } diff --git a/tests/PHPStan/Analyser/data/callable-in-union.php b/tests/PHPStan/Analyser/data/callable-in-union.php new file mode 100644 index 0000000000..b72c0725cc --- /dev/null +++ b/tests/PHPStan/Analyser/data/callable-in-union.php @@ -0,0 +1,17 @@ +|(callable(array): array) $_ */ +function acceptArrayOrCallable($_) +{ +} + +acceptArrayOrCallable(fn ($parameter) => assertType('array', $parameter)); + +acceptArrayOrCallable(function ($parameter) { + assertType('array', $parameter); + return $parameter; +}); diff --git a/tests/PHPStan/Analyser/data/cast-to-numeric-string.php b/tests/PHPStan/Analyser/data/cast-to-numeric-string.php index e9e2259cb7..5014f87302 100644 --- a/tests/PHPStan/Analyser/data/cast-to-numeric-string.php +++ b/tests/PHPStan/Analyser/data/cast-to-numeric-string.php @@ -13,13 +13,13 @@ * @param 1 $constantInt */ function foo(int $a, float $b, $numeric, $numeric2, $number, $positive, $negative, $constantInt): void { - assertType('string&numeric', (string)$a); - assertType('string&numeric', (string)$b); - assertType('string&numeric', (string)$numeric); - assertType('string&numeric', (string)$numeric2); - assertType('string&numeric', (string)$number); - assertType('string&numeric', (string)$positive); - assertType('string&numeric', (string)$negative); + assertType('numeric-string', (string)$a); + assertType('numeric-string', (string)$b); + assertType('numeric-string', (string)$numeric); + assertType('numeric-string', (string)$numeric2); + assertType('numeric-string', (string)$number); + assertType('numeric-string', (string)$positive); + assertType('numeric-string', (string)$negative); assertType("'1'", (string)$constantInt); } @@ -32,30 +32,30 @@ function foo(int $a, float $b, $numeric, $numeric2, $number, $positive, $negativ * @param 1 $constantInt */ function concatEmptyString(int $a, float $b, $numeric, $numeric2, $number, $positive, $negative, $constantInt): void { - assertType('string&numeric', '' . $a); - assertType('string&numeric', '' . $b); - assertType('string&numeric', '' . $numeric); - assertType('string&numeric', '' . $numeric2); - assertType('string&numeric', '' . $number); - assertType('string&numeric', '' . $positive); - assertType('string&numeric', '' . $negative); + assertType('numeric-string', '' . $a); + assertType('numeric-string', '' . $b); + assertType('numeric-string', '' . $numeric); + assertType('numeric-string', '' . $numeric2); + assertType('numeric-string', '' . $number); + assertType('numeric-string', '' . $positive); + assertType('numeric-string', '' . $negative); assertType("'1'", '' . $constantInt); - assertType('string&numeric', $a . ''); - assertType('string&numeric', $b . ''); - assertType('string&numeric', $numeric . ''); - assertType('string&numeric', $numeric2 . ''); - assertType('string&numeric', $number . ''); - assertType('string&numeric', $positive . ''); - assertType('string&numeric', $negative . ''); + assertType('numeric-string', $a . ''); + assertType('numeric-string', $b . ''); + assertType('numeric-string', $numeric . ''); + assertType('numeric-string', $numeric2 . ''); + assertType('numeric-string', $number . ''); + assertType('numeric-string', $positive . ''); + assertType('numeric-string', $negative . ''); assertType("'1'", $constantInt . ''); } function concatAssignEmptyString(int $i, float $f) { $i .= ''; - assertType('string&numeric', $i); + assertType('numeric-string', $i); $s = ''; $s .= $f; - assertType('string&numeric', $s); + assertType('numeric-string', $s); } diff --git a/tests/PHPStan/Analyser/data/catch-without-variable.php b/tests/PHPStan/Analyser/data/catch-without-variable.php index 11b4ef80bd..0ac5a4110a 100644 --- a/tests/PHPStan/Analyser/data/catch-without-variable.php +++ b/tests/PHPStan/Analyser/data/catch-without-variable.php @@ -4,13 +4,18 @@ use function PHPStan\Testing\assertType; +interface I +{ + function test(): void; +} + class Foo { - public function doFoo(): void + public function doFoo(I $i): void { try { - + $i->test(); } catch (\FooException) { assertType('*ERROR*', $e); } diff --git a/tests/PHPStan/Analyser/data/class-constant-stub-files.php b/tests/PHPStan/Analyser/data/class-constant-stub-files.php new file mode 100644 index 0000000000..4777b0ec22 --- /dev/null +++ b/tests/PHPStan/Analyser/data/class-constant-stub-files.php @@ -0,0 +1,24 @@ + + */ +interface ResultStatement extends \Traversable +{ + +} + +interface Statement extends ResultStatement +{ + +} + +function (Statement $s): void +{ + foreach ($s as $k => $v) { + assertType('int', $k); + assertType('mixed', $v); + } +}; diff --git a/tests/PHPStan/Analyser/data/classPhpDocs-phpstanPropertyPrefix.php b/tests/PHPStan/Analyser/data/classPhpDocs-phpstanPropertyPrefix.php new file mode 100644 index 0000000000..f08edc152b --- /dev/null +++ b/tests/PHPStan/Analyser/data/classPhpDocs-phpstanPropertyPrefix.php @@ -0,0 +1,28 @@ +base); + assertType('int', $this->foo); + assertType('int', $this->bar); + assertType('int', $this->baz); + } +} diff --git a/tests/PHPStan/Analyser/data/closure-return-type.php b/tests/PHPStan/Analyser/data/closure-return-type.php index c06880defa..f71b056a55 100644 --- a/tests/PHPStan/Analyser/data/closure-return-type.php +++ b/tests/PHPStan/Analyser/data/closure-return-type.php @@ -27,7 +27,7 @@ public function doFoo(int $i): void $f = function (): array { return ['foo' => 'bar']; }; - assertType('array(\'foo\' => \'bar\')', $f()); + assertType('array{foo: \'bar\'}', $f()); $f = function (string $s) { return $s; diff --git a/tests/PHPStan/Analyser/data/closure-types.php b/tests/PHPStan/Analyser/data/closure-types.php index a0f60a0680..72adfe762a 100644 --- a/tests/PHPStan/Analyser/data/closure-types.php +++ b/tests/PHPStan/Analyser/data/closure-types.php @@ -13,14 +13,14 @@ class Foo public function doFoo(): void { $a = array_map(function (array $a): array { - assertType('array(\'foo\' => string, \'bar\' => int)', $a); + assertType('array{foo: string, bar: int}', $a); return $a; }, $this->arrayShapes); - assertType('array string, \'bar\' => int)>', $a); + assertType('array', $a); $b = array_map(function ($b) { - assertType('array(\'foo\' => string, \'bar\' => int)', $b); + assertType('array{foo: string, bar: int}', $b); return $b['foo']; }, $this->arrayShapes); @@ -30,8 +30,8 @@ public function doFoo(): void public function doBar(): void { usort($this->arrayShapes, function (array $a, array $b): int { - assertType('array(\'foo\' => string, \'bar\' => int)', $a); - assertType('array(\'foo\' => string, \'bar\' => int)', $b); + assertType('array{foo: string, bar: int}', $a); + assertType('array{foo: string, bar: int}', $b); return 1; }); @@ -40,8 +40,8 @@ public function doBar(): void public function doBaz(): void { usort($this->arrayShapes, function ($a, $b): int { - assertType('array(\'foo\' => string, \'bar\' => int)', $a); - assertType('array(\'foo\' => string, \'bar\' => int)', $b); + assertType('array{foo: string, bar: int}', $a); + assertType('array{foo: string, bar: int}', $b); return 1; }); diff --git a/tests/PHPStan/Analyser/data/compact.php b/tests/PHPStan/Analyser/data/compact.php index 4c29db6781..b15f2f5eb4 100644 --- a/tests/PHPStan/Analyser/data/compact.php +++ b/tests/PHPStan/Analyser/data/compact.php @@ -4,7 +4,7 @@ use function PHPStan\Testing\assertType; -assertType('array(?\'bar\' => mixed)', compact(['foo' => 'bar'])); +assertType('array{bar?: mixed}', compact(['foo' => 'bar'])); function (string $dolor): void { $foo = 'bar'; @@ -12,11 +12,11 @@ function (string $dolor): void { if (rand(0, 1)) { $lorem = 'ipsum'; } - assertType('array(\'foo\' => \'bar\', \'bar\' => \'baz\')', compact('foo', ['bar'])); - assertType('array(\'foo\' => \'bar\', \'bar\' => \'baz\', ?\'lorem\' => \'ipsum\')', compact([['foo']], 'bar', 'lorem')); + assertType('array{foo: \'bar\', bar: \'baz\'}', compact('foo', ['bar'])); + assertType('array{foo: \'bar\', bar: \'baz\', lorem?: \'ipsum\'}', compact([['foo']], 'bar', 'lorem')); assertType('array', compact($dolor)); assertType('array', compact([$dolor])); - assertType('array()', compact([])); + assertType('array{}', compact([])); }; diff --git a/tests/PHPStan/Analyser/data/comparison-operators.php b/tests/PHPStan/Analyser/data/comparison-operators.php index 6f5761498c..14488df983 100644 --- a/tests/PHPStan/Analyser/data/comparison-operators.php +++ b/tests/PHPStan/Analyser/data/comparison-operators.php @@ -126,7 +126,7 @@ public function null(?int $i, ?float $f, ?string $s, ?bool $b): void } if ($s > null) { - assertType('string', $s); + assertType('non-empty-string', $s); } if ($s >= null) { assertType('string|null', $s); diff --git a/tests/PHPStan/Analyser/data/conditional-non-empty-array.php b/tests/PHPStan/Analyser/data/conditional-non-empty-array.php index e13d22e3a0..7f3ede3f30 100644 --- a/tests/PHPStan/Analyser/data/conditional-non-empty-array.php +++ b/tests/PHPStan/Analyser/data/conditional-non-empty-array.php @@ -19,10 +19,10 @@ public function doFoo(array $a): void assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); if (count($a) > 0) { - assertType('array&nonEmpty', $a); + assertType('non-empty-array', $a); assertVariableCertainty(TrinaryLogic::createYes(), $foo); } else { - assertType('array()', $a); + assertType('array{}', $a); assertVariableCertainty(TrinaryLogic::createNo(), $foo); } } diff --git a/tests/PHPStan/Analyser/data/const-expr-phpdoc-type.php b/tests/PHPStan/Analyser/data/const-expr-phpdoc-type.php index 317c91c7b9..ab79fb6a46 100644 --- a/tests/PHPStan/Analyser/data/const-expr-phpdoc-type.php +++ b/tests/PHPStan/Analyser/data/const-expr-phpdoc-type.php @@ -10,6 +10,9 @@ class Foo public const SOME_CONSTANT = 1; public const SOME_OTHER_CONSTANT = 2; + public const YET_ONE_CONST = 3; + public const YET_ANOTHER_CONST = 4; + public const DUMMY_SOME_CONSTANT = 99; // should only match the * /** * @param 'foo'|'bar' $one @@ -21,6 +24,9 @@ class Foo * @param 234 $seven * @param self::SOME_OTHER_* $eight * @param self::* $nine + * @param self::*_CONST $ten + * @param self::YET_*_CONST $eleven + * @param self::*OTHER* $twelve */ public function doFoo( $one, @@ -31,7 +37,10 @@ public function doFoo( $six, $seven, $eight, - $nine + $nine, + $ten, + $eleven, + $twelve ) { assertType("'bar'|'foo'", $one); @@ -42,7 +51,10 @@ public function doFoo( assertType('1.0', $six); assertType('234', $seven); assertType('2', $eight); - assertType('1|2', $nine); + assertType('1|2|3|4|99', $nine); + assertType('3|4', $ten); + assertType('3|4', $eleven); + assertType('2|4', $twelve); } } diff --git a/tests/PHPStan/Analyser/data/constant-array-type-set.php b/tests/PHPStan/Analyser/data/constant-array-type-set.php new file mode 100644 index 0000000000..9ae0b88828 --- /dev/null +++ b/tests/PHPStan/Analyser/data/constant-array-type-set.php @@ -0,0 +1,117 @@ +', $a); + + $b = [1, 2, 3]; + $b[3] = 4; + assertType('array{1, 2, 3, 4}', $b); + + $c = [false, false, false]; + /** @var 0|1|2 $offset */ + $offset = doFoo(); + $c[$offset] = true; + assertType('array{bool, bool, bool}', $c); + + $d = [false, false, false]; + /** @var int<0, 2> $offset2 */ + $offset2 = doFoo(); + $d[$offset2] = true; + assertType('array{bool, bool, bool}', $d); + + $e = [false, false, false]; + /** @var 0|1|2|3 $offset3 */ + $offset3 = doFoo(); + $e[$offset3] = true; + assertType('non-empty-array<0|1|2|3, bool>', $e); + + $f = [false, false, false]; + /** @var 0|1 $offset4 */ + $offset4 = doFoo(); + $f[$offset4] = true; + assertType('array{bool, bool, false}', $f); + } + + /** + * @param int<0, 1> $offset + * @return void + */ + public function doBar(int $offset): void + { + $a = [false, false, false]; + $a[$offset] = true; + assertType('array{bool, bool, false}', $a); + } + + /** + * @param int<0, 1>|int<3, 4> $offset + * @return void + */ + public function doBar2(int $offset): void + { + $a = [false, false, false, false, false]; + $a[$offset] = true; + assertType('array{bool, bool, false, bool, bool}', $a); + } + + /** + * @param int<0, max> $offset + * @return void + */ + public function doBar3(int $offset): void + { + $a = [false, false, false, false, false]; + $a[$offset] = true; + assertType('non-empty-array, bool>', $a); + } + + /** + * @param int $offset + * @return void + */ + public function doBar4(int $offset): void + { + $a = [false, false, false, false, false]; + $a[$offset] = true; + assertType('non-empty-array, bool>', $a); + } + + /** + * @param int<0, 4> $offset + * @return void + */ + public function doBar5(int $offset): void + { + $a = [false, false, false]; + $a[$offset] = true; + assertType('non-empty-array, bool>', $a); + } + + public function doBar6(bool $offset): void + { + $a = [false, false, false]; + $a[$offset] = true; + assertType('array{bool, bool, false}', $a); + } + + /** + * @param true $offset + */ + public function doBar7(bool $offset): void + { + $a = [false, false, false]; + $a[$offset] = true; + assertType('array{false, true, false}', $a); + } + +} diff --git a/tests/PHPStan/Analyser/data/count-type.php b/tests/PHPStan/Analyser/data/count-type.php index 5f99ef117e..58dadbdd4a 100644 --- a/tests/PHPStan/Analyser/data/count-type.php +++ b/tests/PHPStan/Analyser/data/count-type.php @@ -15,6 +15,7 @@ public function doFoo( ) { assertType('int<1, max>', count($nonEmpty)); + assertType('int<1, max>', sizeof($nonEmpty)); } } diff --git a/tests/PHPStan/Analyser/data/countable.php b/tests/PHPStan/Analyser/data/countable.php new file mode 100644 index 0000000000..2d18e25faa --- /dev/null +++ b/tests/PHPStan/Analyser/data/countable.php @@ -0,0 +1,17 @@ +', $foo->count()); + } +} + diff --git a/tests/PHPStan/Analyser/data/date-format.php b/tests/PHPStan/Analyser/data/date-format.php new file mode 100644 index 0000000000..92253edb14 --- /dev/null +++ b/tests/PHPStan/Analyser/data/date-format.php @@ -0,0 +1,45 @@ +format('')); + assertType('string', $dt->format($s)); + assertType('non-empty-string', $dt->format('D')); + assertType('numeric-string', $dt->format('Y')); + assertType('numeric-string', $dt->format('Ghi')); +}; + +function (\DateTime $dt, string $s): void { + assertType('\'\'', $dt->format('')); + assertType('string', $dt->format($s)); + assertType('non-empty-string', $dt->format('D')); + assertType('numeric-string', $dt->format('Y')); + assertType('numeric-string', $dt->format('Ghi')); +}; + +function (\DateTimeImmutable $dt, string $s): void { + assertType('\'\'', $dt->format('')); + assertType('string', $dt->format($s)); + assertType('non-empty-string', $dt->format('D')); + assertType('numeric-string', $dt->format('Y')); + assertType('numeric-string', $dt->format('Ghi')); +}; diff --git a/tests/PHPStan/Analyser/data/date-period-return-types.php b/tests/PHPStan/Analyser/data/date-period-return-types.php new file mode 100644 index 0000000000..17a08cbd18 --- /dev/null +++ b/tests/PHPStan/Analyser/data/date-period-return-types.php @@ -0,0 +1,40 @@ +', $datePeriod); +assertType(\DateTime::class, $datePeriod->getEndDate()); +assertType('null', $datePeriod->getRecurrences()); +$datePeriodList[] = $datePeriod; + +foreach ($datePeriod as $k => $v) { + assertType('int', $k); + assertType('DateTime', $v); +} + +$datePeriod = new DatePeriod($start, $interval, $recurrences); +assertType(\DatePeriod::class . '', $datePeriod); +assertType('null', $datePeriod->getEndDate()); +assertType('4', $datePeriod->getRecurrences()); +$datePeriodList[] = $datePeriod; + +$datePeriod = new DatePeriod($iso); +assertType(\DatePeriod::class . '', $datePeriod); +assertType('null', $datePeriod->getEndDate()); +assertType('int', $datePeriod->getRecurrences()); +$datePeriodList[] = $datePeriod; + +/** @var DatePeriod $datePeriod */ +$datePeriod = $datePeriodList[random_int(0, 2)]; +assertType(\DatePeriod::class, $datePeriod); +assertType(\DateTimeInterface::class . '|null', $datePeriod->getEndDate()); +assertType('int|null', $datePeriod->getRecurrences()); diff --git a/tests/PHPStan/Analyser/data/date.php b/tests/PHPStan/Analyser/data/date.php new file mode 100644 index 0000000000..e734a3ebab --- /dev/null +++ b/tests/PHPStan/Analyser/data/date.php @@ -0,0 +1,34 @@ +', $itemsCounter); } assertType('Generator&iterable', $associationData); - assertType('int', $itemsCounter); + assertType('int<0, max>', $itemsCounter); } } diff --git a/tests/PHPStan/Analyser/data/div-by-zero.php b/tests/PHPStan/Analyser/data/div-by-zero.php new file mode 100644 index 0000000000..2dae4d4767 --- /dev/null +++ b/tests/PHPStan/Analyser/data/div-by-zero.php @@ -0,0 +1,28 @@ + $range1 + * @param int $range2 + */ + public function doFoo(int $range1, int $range2, int $int): void + { + assertType('(float|int)', 5 / $range1); + assertType('(float|int)', 5 / $range2); + assertType('(float|int)', $range1 / $range2); + assertType('(float|int)', 5 / $int); + + assertType('*ERROR*', 5 / 0); + assertType('*ERROR*', 5 / '0'); + assertType('*ERROR*', 5 / 0.0); + assertType('*ERROR*', 5 / false); + assertType('*ERROR*', 5 / null); + } + +} diff --git a/tests/PHPStan/Analyser/data/do-not-remember-impure-functions.php b/tests/PHPStan/Analyser/data/do-not-remember-impure-functions.php index 498a37a7de..4b086bc15c 100644 --- a/tests/PHPStan/Analyser/data/do-not-remember-impure-functions.php +++ b/tests/PHPStan/Analyser/data/do-not-remember-impure-functions.php @@ -11,18 +11,18 @@ public function doFoo() { function (): void { if (rand(0, 1)) { - assertType('int', rand(0, 1)); + assertType('int<0, 1>', rand(0, 1)); } }; function (): void { if (rand(0, 1) === 0) { - assertType('int', rand(0, 1)); + assertType('int<0, 1>', rand(0, 1)); } }; function (): void { - assertType('\'foo\'|int|int<1, max>', rand(0, 1) ?: 'foo'); - assertType('\'foo\'|int', rand(0, 1) ? rand(0, 1) : 'foo'); + assertType('1|\'foo\'', rand(0, 1) ?: 'foo'); + assertType('\'foo\'|int<0, 1>', rand(0, 1) ? rand(0, 1) : 'foo'); }; } diff --git a/tests/PHPStan/Analyser/data/dynamic-method-return-compound-types.php b/tests/PHPStan/Analyser/data/dynamic-method-return-compound-types.php index 2c35369686..5d163a1eda 100644 --- a/tests/PHPStan/Analyser/data/dynamic-method-return-compound-types.php +++ b/tests/PHPStan/Analyser/data/dynamic-method-return-compound-types.php @@ -2,6 +2,8 @@ namespace DynamicMethodReturnCompoundTypes; +use function PHPStan\Testing\assertType; + interface Collection extends \Traversable { @@ -23,7 +25,8 @@ public function getSelf() */ public function doFoo($collection, $collectionOrFoo) { - die; + assertType('DynamicMethodReturnCompoundTypes\Collection', $collection->getSelf()); + assertType('DynamicMethodReturnCompoundTypes\Collection|DynamicMethodReturnCompoundTypes\Foo', $collectionOrFoo->getSelf()); } } diff --git a/tests/PHPStan/Analyser/data/dynamic-method-return-types.php b/tests/PHPStan/Analyser/data/dynamic-method-return-types.php index e32d36e329..0fb7c70d6d 100644 --- a/tests/PHPStan/Analyser/data/dynamic-method-return-types.php +++ b/tests/PHPStan/Analyser/data/dynamic-method-return-types.php @@ -2,6 +2,8 @@ namespace DynamicMethodReturnTypesNamespace; +use function PHPStan\Testing\assertType; + class EntityManager { @@ -25,6 +27,7 @@ class InheritedEntityManager extends EntityManager class ComponentContainer implements \ArrayAccess { + #[\ReturnTypeWillChange] public function offsetExists($offset) { @@ -35,11 +38,13 @@ public function offsetGet($offset): Entity } + #[\ReturnTypeWillChange] public function offsetSet($offset, $value) { } + #[\ReturnTypeWillChange] public function offsetUnset($offset) { @@ -59,7 +64,30 @@ public function doFoo() $em = new EntityManager(); $iem = new InheritedEntityManager(); $container = new ComponentContainer(); - die; + + assertType('*ERROR*', $em->getByFoo($foo)); + assertType('DynamicMethodReturnTypesNamespace\Entity', $em->getByPrimary()); + assertType('DynamicMethodReturnTypesNamespace\Entity', $em->getByPrimary($foo)); + assertType('DynamicMethodReturnTypesNamespace\Foo', $em->getByPrimary(\DynamicMethodReturnTypesNamespace\Foo::class)); + + assertType('*ERROR*', $iem->getByFoo($foo)); + assertType('DynamicMethodReturnTypesNamespace\Entity', $iem->getByPrimary()); + assertType('DynamicMethodReturnTypesNamespace\Entity', $iem->getByPrimary($foo)); + assertType('DynamicMethodReturnTypesNamespace\Foo', $iem->getByPrimary(\DynamicMethodReturnTypesNamespace\Foo::class)); + + assertType('*ERROR*', EntityManager::getByFoo($foo)); + assertType('DynamicMethodReturnTypesNamespace\EntityManager', \DynamicMethodReturnTypesNamespace\EntityManager::createManagerForEntity()); + assertType('DynamicMethodReturnTypesNamespace\EntityManager', \DynamicMethodReturnTypesNamespace\EntityManager::createManagerForEntity($foo)); + assertType('DynamicMethodReturnTypesNamespace\Foo', \DynamicMethodReturnTypesNamespace\EntityManager::createManagerForEntity(\DynamicMethodReturnTypesNamespace\Foo::class)); + + assertType('*ERROR*', InheritedEntityManager::getByFoo($foo)); + assertType('DynamicMethodReturnTypesNamespace\EntityManager', \DynamicMethodReturnTypesNamespace\InheritedEntityManager::createManagerForEntity()); + assertType('DynamicMethodReturnTypesNamespace\EntityManager', \DynamicMethodReturnTypesNamespace\InheritedEntityManager::createManagerForEntity($foo)); + assertType('DynamicMethodReturnTypesNamespace\Foo', \DynamicMethodReturnTypesNamespace\InheritedEntityManager::createManagerForEntity(\DynamicMethodReturnTypesNamespace\Foo::class)); + + assertType('DynamicMethodReturnTypesNamespace\Foo', $container[\DynamicMethodReturnTypesNamespace\Foo::class]); + assertType('object', new \DynamicMethodReturnTypesNamespace\Foo()); + assertType('object', new \DynamicMethodReturnTypesNamespace\FooWithoutConstructor()); } } diff --git a/tests/PHPStan/Analyser/data/empty-array-shape.php b/tests/PHPStan/Analyser/data/empty-array-shape.php index 4a19bf0f59..35d3dcece9 100644 --- a/tests/PHPStan/Analyser/data/empty-array-shape.php +++ b/tests/PHPStan/Analyser/data/empty-array-shape.php @@ -10,7 +10,7 @@ class Foo /** @param array{} $array */ public function doFoo(array $array): void { - assertType('array()', $array); + assertType('array{}', $array); } } diff --git a/tests/PHPStan/Analyser/data/enums-import-alias.php b/tests/PHPStan/Analyser/data/enums-import-alias.php new file mode 100644 index 0000000000..b18f7879de --- /dev/null +++ b/tests/PHPStan/Analyser/data/enums-import-alias.php @@ -0,0 +1,27 @@ += 8.1 + +namespace EnumTypeAssertionsImportAlias; + +use function PHPStan\Testing\assertType; + +/** + * @phpstan-import-type TypeAlias from \EnumTypeAssertions\EnumWithTypeAliases as TypeAlias2 + */ +enum Foo +{ + + /** + * @param TypeAlias2 $p + * @return TypeAlias2 + */ + public function doFoo($p) + { + assertType('array{foo: int, bar: string}', $p); + } + + public function doBar() + { + assertType('array{foo: int, bar: string}', $this->doFoo()); + } + +} diff --git a/tests/PHPStan/Analyser/data/enums-integration.php b/tests/PHPStan/Analyser/data/enums-integration.php new file mode 100644 index 0000000000..a7b50c774b --- /dev/null +++ b/tests/PHPStan/Analyser/data/enums-integration.php @@ -0,0 +1,85 @@ += 8.1 + +namespace EnumIntegrationTest; + +enum Foo +{ + + case ONE; + case TWO; + +} + + +class FooClass +{ + + public function doFoo(Foo $foo): void + { + $this->doBar($foo); + $this->doBar(Foo::ONE); + $this->doBar(Foo::TWO); + echo Foo::TWO->value; + echo count(Foo::cases()); + } + + public function doBar(Foo $foo): void + { + + } + +} + +enum Bar : string +{ + + case ONE = 'one'; + case TWO = 'two'; + +} + +class BarClass +{ + + public function doFoo(Bar $bar, string $s): void + { + $this->doBar($bar); + $this->doBar(Bar::ONE); + $this->doBar(Bar::TWO); + $this->doBar(Bar::NONEXISTENT); + echo Bar::TWO->value; + echo count(Bar::cases()); + $this->doBar(Bar::from($s)); + $this->doBar(Bar::tryFrom($s)); + } + + public function doBar(?Bar $bar): void + { + + } + +} + +enum Baz : int +{ + + case ONE = 1; + case TWO = 2; + const THREE = 3; + const FOUR = 4; + +} + +class Lorem +{ + + public function doBaz(Foo $foo): void + { + if ($foo === Foo::ONE) { + if ($foo === Foo::TWO) { + + } + } + } + +} diff --git a/tests/PHPStan/Analyser/data/enums.php b/tests/PHPStan/Analyser/data/enums.php new file mode 100644 index 0000000000..1545a53afc --- /dev/null +++ b/tests/PHPStan/Analyser/data/enums.php @@ -0,0 +1,266 @@ += 8.1 + +namespace EnumTypeAssertions; + +use function PHPStan\Testing\assertType; + +enum Foo +{ + + case ONE; + case TWO; + + public function doFoo(): void + { + if ($this === self::ONE) { + assertType(self::class . '::ONE', $this); + return; + } + + assertType(self::class . '::TWO', $this); + } + +} + + +class FooClass +{ + + public function doFoo(Foo $foo): void + { + assertType(Foo::class . '::ONE' , Foo::ONE); + assertType(Foo::class . '::TWO', Foo::TWO); + assertType('*ERROR*', Foo::TWO->value); + assertType('array{EnumTypeAssertions\Foo::ONE, EnumTypeAssertions\Foo::TWO}', Foo::cases()); + assertType("'ONE'|'TWO'", $foo->name); + assertType("'ONE'", Foo::ONE->name); + assertType("'TWO'", Foo::TWO->name); + } + +} + +enum Bar : string +{ + + case ONE = 'one'; + case TWO = 'two'; + +} + +class BarClass +{ + + public function doFoo(string $s, Bar $bar): void + { + assertType(Bar::class . '::ONE', Bar::ONE); + assertType(Bar::class . '::TWO', Bar::TWO); + assertType('\'two\'', Bar::TWO->value); + assertType('array{EnumTypeAssertions\Bar::ONE, EnumTypeAssertions\Bar::TWO}', Bar::cases()); + + assertType(Bar::class, Bar::from($s)); + assertType(Bar::class . '|null', Bar::tryFrom($s)); + + assertType("'one'|'two'", $bar->value); + } + +} + +enum Baz : int +{ + + case ONE = 1; + case TWO = 2; + const THREE = 3; + const FOUR = 4; + +} + +class BazClass +{ + + public function doFoo(int $i, Baz $baz): void + { + assertType(Baz::class . '::ONE', Baz::ONE); + assertType(Baz::class . '::TWO', Baz::TWO); + assertType('2', Baz::TWO->value); + assertType('array{EnumTypeAssertions\Baz::ONE, EnumTypeAssertions\Baz::TWO}', Baz::cases()); + + assertType(Baz::class, Baz::from($i)); + assertType(Baz::class . '|null', Baz::tryFrom($i)); + + assertType('3', Baz::THREE); + assertType('4', Baz::FOUR); + assertType('*ERROR*', Baz::NONEXISTENT); + + assertType('1|2', $baz->value); + assertType('1', Baz::ONE->value); + assertType('2', Baz::TWO->value); + } + + /** + * @param Baz::ONE $enum + * @param Baz::THREE $constant + * @return void + */ + public function doBar($enum, $constant): void + { + assertType(Baz::class . '::ONE', $enum); + assertType('3', $constant); + } + + /** + * @param Baz::ONE $enum + * @param Baz::THREE $constant + * @return void + */ + public function doBaz(Baz $enum, $constant): void + { + assertType(Baz::class . '::ONE', $enum); + assertType('3', $constant); + } + + /** + * @param Foo::* $enums + * @return void + */ + public function doLorem($enums): void + { + assertType(Foo::class . '::ONE|' . Foo::class . '::TWO', $enums); + } + +} + +class Lorem +{ + + public function doFoo(Foo $foo): void + { + if ($foo === Foo::ONE) { + assertType(Foo::class . '::ONE', $foo); + return; + } + + assertType(Foo::class . '::TWO', $foo); + } + + public function doBar(Foo $foo): void + { + if (Foo::ONE === $foo) { + assertType(Foo::class . '::ONE', $foo); + return; + } + + assertType(Foo::class . '::TWO', $foo); + } + + public function doBaz(Foo $foo): void + { + if ($foo === Foo::ONE) { + assertType(Foo::class . '::ONE', $foo); + if ($foo === Foo::TWO) { + assertType('*NEVER*', $foo); + } else { + assertType(Foo::class . '::ONE', $foo); + } + + assertType(Foo::class . '::ONE', $foo); + } + } + + public function doClass(Foo $foo): void + { + assertType('class-string<' . Foo::class . '>', $foo::class); + assertType(Foo::class . '::ONE', Foo::ONE); + assertType('class-string<' . Foo::class . '>', Foo::ONE::class); + assertType(Bar::class . '::ONE', Bar::ONE); + assertType('class-string<' . Bar::class . '>', Bar::ONE::class); + } + +} + +class EnumInConst +{ + + const TEST = [Foo::ONE]; + + public function doFoo() + { + assertType('array{EnumTypeAssertions\Foo::ONE}', self::TEST); + } + +} + +/** @template T */ +interface GenericInterface +{ + + /** @return T */ + public function doFoo(); + +} + +/** @implements GenericInterface */ +enum EnumImplementsGeneric: int implements GenericInterface +{ + + case ONE = 1; + + public function doFoo() + { + return 1; + } + +} + +class TestEnumImplementsGeneric +{ + + public function doFoo(EnumImplementsGeneric $e): void + { + assertType('int', $e->doFoo()); + assertType('int', EnumImplementsGeneric::ONE->doFoo()); + } + +} + +class MixedMethod +{ + + public function doFoo(): int + { + return 1; + } + +} + +/** @mixin MixedMethod */ +enum EnumWithMixin +{ + +} + +function (EnumWithMixin $i): void { + assertType('int', $i->doFoo()); +}; + +/** + * @phpstan-type TypeAlias array{foo: int, bar: string} + */ +enum EnumWithTypeAliases +{ + + /** + * @param TypeAlias $p + * @return TypeAlias + */ + public function doFoo($p) + { + assertType('array{foo: int, bar: string}', $p); + } + + public function doBar() + { + assertType('array{foo: int, bar: string}', $this->doFoo()); + } + +} diff --git a/tests/PHPStan/Analyser/data/equal.php b/tests/PHPStan/Analyser/data/equal.php new file mode 100644 index 0000000000..aad0f3ef5b --- /dev/null +++ b/tests/PHPStan/Analyser/data/equal.php @@ -0,0 +1,167 @@ +|int<8, 13> $i */ + public function doBaz(int $i): void + { + assertType('int<1, 3>|int<8, 13>', $i); + if ($i == 3) { + assertType('3', $i); + } else { + assertType('int<1, 2>|int<8, 13>', $i); + } + assertType('int<1, 3>|int<8, 13>', $i); + } + + public function doLorem(float $f): void + { + assertType('float', $f); + if ($f == 3.5) { + assertType('3.5', $f); + } else { + assertType('float', $f); + } + + assertType('float', $f); + } + + public function doIpsum(array $a): void + { + assertType('array', $a); + if ($a == []) { + assertType('array{}', $a); + } else { + assertType('non-empty-array', $a); + } + assertType('array', $a); + } + + public function stdClass(\stdClass $a, \stdClass $b): void + { + if ($a == $a) { + assertType('stdClass', $a); + } else { + assertType('*NEVER*', $a); + } + + if ($b != $b) { + assertType('*NEVER*', $b); + } else { + assertType('stdClass', $b); + } + + if ($a == $b) { + assertType('stdClass', $a); + assertType('stdClass', $b); + } else { + assertType('stdClass', $a); + assertType('stdClass', $b); + } + + if ($a != $b) { + assertType('stdClass', $a); + assertType('stdClass', $b); + } else { + assertType('stdClass', $a); + assertType('stdClass', $b); + } + + assertType('stdClass', $a); + assertType('stdClass', $b); + } + + /** + * @param array{a: string, b: array{c: string|null}} $a + */ + public function arrayOffset(array $a): void + { + if (strlen($a['a']) > 0 && $a['a'] === $a['b']['c']) { + assertType('array{a: non-empty-string, b: array{c: non-empty-string}}', $a); + } + } + +} + +class Bar +{ + + public function doFoo(\stdClass $a, \stdClass $b): void + { + assertType('true', $a == $a); + assertType('bool', $a == $b); + assertType('false', $a != $a); + assertType('bool', $a != $b); + + assertType('bool', self::createStdClass() == self::createStdClass()); + assertType('bool', self::createStdClass() != self::createStdClass()); + } + + public static function createStdClass(): \stdClass + { + + } + +} + +class Baz +{ + + public function doFoo(string $a, int $b, float $c): void + { + $nullableA = $a; + if (rand(0, 1)) { + $nullableA = null; + } + + assertType('bool', $a == $nullableA); + assertType('bool', $a == 'a'); + assertType('true', 'a' == 'a'); + assertType('false', 'a' == 'b'); + + assertType('bool', $a != $nullableA); + assertType('bool', $a != 'a'); + assertType('false', 'a' != 'a'); + assertType('true', 'a' != 'b'); + + assertType('bool', $b == 'a'); + assertType('bool', $a == 1); + assertType('true', 1 == 1); + assertType('false', 1 == 0); + + assertType('bool', $c == 'a'); + assertType('bool', $c == 1); + assertType('bool', $c == 1.2); + assertType('true', 1.2 == 1.2); + assertType('false', 1.2 == 1.3); + } + +} diff --git a/tests/PHPStan/Analyser/data/eval-implicit-throw.php b/tests/PHPStan/Analyser/data/eval-implicit-throw.php new file mode 100644 index 0000000000..a95aaca46e --- /dev/null +++ b/tests/PHPStan/Analyser/data/eval-implicit-throw.php @@ -0,0 +1,16 @@ += 8.1 + +namespace FirstClassCallables; + +use PHPStan\TrinaryLogic; +use function PHPStan\Testing\assertType; +use function PHPStan\Testing\assertVariableCertainty; + +class Foo +{ + + public function doFoo(string $foo): void + { + assertType('Closure(string): void', $this->doFoo(...)); + assertType('Closure(): void', self::doBar(...)); + assertType('Closure', self::$foo(...)); + assertType('Closure', $this->nonexistent(...)); + assertType('Closure', $this->$foo(...)); + assertType('Closure(string): int<0, max>', strlen(...)); + assertType('Closure(string): int<0, max>', 'strlen'(...)); + assertType('Closure', 'nonexistent'(...)); + } + + public static function doBar(): void + { + + } + +} + +class GenericFoo +{ + + /** + * @template T + * @param T $a + * @return T + */ + public function doFoo($a) + { + return $a; + } + + public function doBar() + { + $f = $this->doFoo(...); + assertType('int', $f(1)); + assertType('string', $f('foo')); + + $g = \Closure::fromCallable([$this, 'doFoo']); + assertType('int', $g(1)); + assertType('string', $g('foo')); + } + + public function doBaz() + { + $ref = new \ReflectionClass(\stdClass::class); + assertType('class-string', $ref->getName()); + + $f = $ref->getName(...); + assertType('class-string', $f()); + + $g = \Closure::fromCallable([$ref, 'getName']); + assertType('class-string', $g()); + } + +} + +class NeverCallable +{ + + public function doFoo() + { + $n = function (): never { + throw new \Exception(); + }; + + if (rand(0, 1)) { + $n(); + } else { + $foo = 1; + } + + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + } + + public function doBar() + { + $n = function (): never { + throw new \Exception(); + }; + + if (rand(0, 1)) { + $n(...); + } else { + $foo = 1; + } + + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); + } + + /** + * @param callable(): never $n + */ + public function doBaz(callable $n): void + { + if (rand(0, 1)) { + $n(); + } else { + $foo = 1; + } + + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + } + +} diff --git a/tests/PHPStan/Analyser/data/for-loop-i-type.php b/tests/PHPStan/Analyser/data/for-loop-i-type.php new file mode 100644 index 0000000000..0e602ba293 --- /dev/null +++ b/tests/PHPStan/Analyser/data/for-loop-i-type.php @@ -0,0 +1,97 @@ +', $i); + } + + assertType('int<50, max>', $i); + assertType(\stdClass::class, $foo); + + for($i = 50; $i > 0; $i--) { + assertType('int<1, 50>', $i); + } + + assertType('int', $i); + } + + public function doCount(array $a) { + $foo = null; + for($i = 1; $i < count($a); $i++) { + $foo = new \stdClass(); + assertType('int<1, max>', $i); + } + + assertType('int<1, max>', $i); + assertType(\stdClass::class . '|null', $foo); + } + + public function doCount2() { + $foo = null; + for($i = 1; $i < count([]); $i++) { + $foo = new \stdClass(); + assertType('string', $i); // should be *NEVER* + } + + assertType('1', $i); + assertType('null', $foo); + } + + public function doBaz() { + for($i = 1; $i < 50; $i += 2) { + assertType('1|int<3, 49>', $i); + } + + assertType('int<50, max>', $i); + } + + public function doLOrem() { + for($i = 1; $i < 50; $i++) { + break; + } + + assertType('int<1, max>', $i); + } + +} + +interface Foo2 { + function equals(self $other): bool; +} + +class HelloWorld +{ + /** + * @param Foo2[] $startTimes + * @return mixed[] + */ + public static function groupCapacities(array $startTimes): array + { + if ($startTimes === []) { + return []; + } + sort($startTimes); + + $capacities = []; + $current = $startTimes[0]; + $count = 0; + foreach ($startTimes as $startTime) { + if (!$startTime->equals($current)) { + $count = 0; + } + $count++; + } + assertType('int<1, max>', $count); + + return $capacities; + } +} diff --git a/tests/PHPStan/Analyser/data/functions.php b/tests/PHPStan/Analyser/data/functions.php index 173a4cd5f5..6ef64e58fc 100644 --- a/tests/PHPStan/Analyser/data/functions.php +++ b/tests/PHPStan/Analyser/data/functions.php @@ -6,12 +6,6 @@ $microtimeDefault = microtime(null); $microtimeBenevolent = microtime($undefined); -$strtotimeNow = strtotime('now'); -$strtotimeInvalid = strtotime('4 qm'); -$strtotimeUnknown = strtotime(doFoo() ? 'now': '4 qm'); -$strtotimeUnknown2 = strtotime($undefined); -$strtotimeCrash = strtotime(); - $versionCompare1 = version_compare('7.0.0', '7.0.1'); $versionCompare2 = version_compare('7.0.0', doFoo() ? '7.0.1' : '6.0.0'); $versionCompare3 = version_compare(doFoo() ? '7.0.0' : '6.0.5', doBar() ? '7.0.1' : '6.0.0'); @@ -101,6 +95,8 @@ $stat = stat(__FILE__); $lstat = lstat(__FILE__); $fstat = fstat($resource); +$fileObject = new \SplFileObject(__FILE__); +$fileObjectStat = $fileObject->fstat(); $base64DecodeWithoutStrict = base64_decode(''); $base64DecodeWithStrictDisabled = base64_decode('', false); @@ -123,22 +119,4 @@ $integer = doFoo(); $strWordCountStrTypeIndeterminant = str_word_count('string', $integer); -$hashHmacMd5 = hash_hmac('md5', 'data', 'key'); -$hashHmacSha256 = hash_hmac('sha256', 'data', 'key'); -$hashHmacNonCryptographic = hash_hmac('crc32', 'data', 'key'); -$hashHmacRandom = hash_hmac('random', 'data', 'key'); -$hashHmacVariable = hash_hmac($string, 'data', 'key'); - -$hashHmacFileMd5 = hash_hmac_file('md5', 'data', 'key'); -$hashHmacFileSha256 = hash_hmac_file('sha256', 'data', 'key'); -$hashHmacFileNonCryptographic = hash_hmac_file('crc32', 'data', 'key'); -$hashHmacFileRandom = hash_hmac_file('random', 'data', 'key'); -$hashHmacFileVariable = hash_hmac_file($string, 'data', 'key'); - -$hash = hash('sha256', 'data', false); -$hashRaw = hash('sha256', 'data', true); -$hashRandom = hash('random', 'data', false); -/** @var mixed $mixed */ -$mixed = doFoo(); -$hashMixed = hash('md5', $mixed, false); die; diff --git a/tests/PHPStan/Analyser/data/generic-class-string.php b/tests/PHPStan/Analyser/data/generic-class-string.php index f7cec7de62..d808a0fcf8 100644 --- a/tests/PHPStan/Analyser/data/generic-class-string.php +++ b/tests/PHPStan/Analyser/data/generic-class-string.php @@ -15,20 +15,27 @@ public static function f(): int { * @param mixed $a */ function testMixed($a) { - assertType('mixed~string', new $a()); + assertType('object', new $a()); if (is_subclass_of($a, 'DateTimeInterface')) { assertType('class-string|DateTimeInterface', $a); assertType('DateTimeInterface', new $a()); + } else { + assertType('mixed~class-string|DateTimeInterface', $a); } if (is_subclass_of($a, 'DateTimeInterface') || is_subclass_of($a, 'stdClass')) { assertType('class-string|class-string|DateTimeInterface|stdClass', $a); assertType('DateTimeInterface|stdClass', new $a()); + } else { + // could also exclude stdClass + assertType('mixed~class-string|DateTimeInterface', $a); } if (is_subclass_of($a, C::class)) { assertType('int', $a::f()); + } else { + assertType('mixed~class-string|PHPStan\Generics\GenericClassStringType\C', $a); } } @@ -36,10 +43,12 @@ function testMixed($a) { * @param object $a */ function testObject($a) { - assertType('mixed~string', new $a()); + assertType('object', new $a()); if (is_subclass_of($a, 'DateTimeInterface')) { assertType('DateTimeInterface', $a); + } else { + assertType('object~DateTimeInterface', $a); } } @@ -47,15 +56,19 @@ function testObject($a) { * @param string $a */ function testString($a) { - assertType('mixed~string', new $a()); + assertType('object', new $a()); if (is_subclass_of($a, 'DateTimeInterface')) { assertType('class-string', $a); assertType('DateTimeInterface', new $a()); + } else { + assertType('string', $a); } if (is_subclass_of($a, C::class)) { assertType('int', $a::f()); + } else { + assertType('string', $a); } } @@ -63,15 +76,19 @@ function testString($a) { * @param string|object $a */ function testStringObject($a) { - assertType('mixed~string', new $a()); + assertType('object', new $a()); if (is_subclass_of($a, 'DateTimeInterface')) { assertType('class-string|DateTimeInterface', $a); assertType('DateTimeInterface', new $a()); + } else { + assertType('object~DateTimeInterface|string', $a); } if (is_subclass_of($a, C::class)) { assertType('int', $a::f()); + } else { + assertType('object~PHPStan\Generics\GenericClassStringType\C|string', $a); } } @@ -84,6 +101,29 @@ function testClassString($a) { if (is_subclass_of($a, 'DateTime')) { assertType('class-string', $a); assertType('DateTime', new $a()); + } else { + assertType('class-string', $a); + } +} + +/** + * @param object|string $a + * @param class-string<\DateTimeInterface> $b + */ +function testClassStringAsClassName($a, string $b) { + assertType('object', new $a()); + + if (is_subclass_of($a, $b)) { + assertType('class-string|DateTimeInterface', $a); + assertType('DateTimeInterface', new $a()); + } else { + assertType('object|string', $a); + } + + if (is_subclass_of($a, $b, false)) { + assertType('DateTimeInterface', $a); + } else { + assertType('object|string', $a); } } @@ -92,7 +132,7 @@ function testClassExists(string $str) assertType('string', $str); if (class_exists($str)) { assertType('class-string', $str); - assertType('mixed~string', new $str()); + assertType('object', new $str()); } $existentClass = \stdClass::class; diff --git a/tests/PHPStan/Analyser/data/generic-generalization.php b/tests/PHPStan/Analyser/data/generic-generalization.php new file mode 100644 index 0000000000..34280cdd25 --- /dev/null +++ b/tests/PHPStan/Analyser/data/generic-generalization.php @@ -0,0 +1,75 @@ + $genericClassString + * @param array{foo: 42} $arrayShape + * @param numeric-string $numericString + * @param non-empty-string $nonEmptyString + */ +function testUnbounded( + string $classString, + string $genericClassString, + string $string, + array $arrayShape, + string $numericString, + string $nonEmptyString +): void { + assertType('string', unbounded('hello')); + assertType('string', unbounded('stdClass')); + assertType('class-string', unbounded($classString)); + assertType('class-string', unbounded($genericClassString)); + + assertType('string', unbounded(rand(0,1) === 1 ? 'hello' : $classString)); + + assertType('array{foo: int}', unbounded($arrayShape)); + + assertType('string', unbounded($numericString)); + assertType('string', unbounded($nonEmptyString)); +} + +/** + * @template T of string + * @param T $arg + * @return T + */ +function boundToString($arg) +{ + return $arg; +} + +/** + * @param class-string $classString + * @param class-string<\stdClass> $genericClassString + * @param non-empty-string $nonEmptyString + */ +function testBoundToString( + string $classString, + string $genericClassString, + string $nonEmptyString, + string $string +): void { + assertType('\'hello\'', boundToString('hello')); + assertType('\'stdClass\'', boundToString('stdClass')); + assertType('class-string', boundToString($classString)); + assertType('class-string', boundToString($genericClassString)); + + assertType('\'hello\'|class-string', boundToString(rand(0,1) === 1 ? 'hello' : $classString)); +} diff --git a/tests/PHPStan/Analyser/data/generic-object-lower-bound.php b/tests/PHPStan/Analyser/data/generic-object-lower-bound.php new file mode 100644 index 0000000000..fe71aab3d6 --- /dev/null +++ b/tests/PHPStan/Analyser/data/generic-object-lower-bound.php @@ -0,0 +1,75 @@ + $c + * @param T $d + * @return T + */ + function doFoo(Collection $c, object $d) + { + $c->add($d); + } + + /** + * @param Collection $c + */ + function doBar(Collection $c): void + { + assertType(Cat::class . '|' . Dog::class, $this->doFoo($c, new Cat())); + } + +} + +class Bar +{ + + /** + * @template T of object + * @param Collection2 $c + * @param T $d + * @return T + */ + function doFoo(Collection2 $c, object $d) + { + $c->add($d); + } + + /** + * @param Collection2 $c + */ + function doBar(Collection2 $c): void + { + assertType(Cat::class . '|' . Dog::class, $this->doFoo($c, new Cat())); + } + +} diff --git a/tests/PHPStan/Analyser/data/generic-offset-get.php b/tests/PHPStan/Analyser/data/generic-offset-get.php new file mode 100644 index 0000000000..8f2cfa4f5a --- /dev/null +++ b/tests/PHPStan/Analyser/data/generic-offset-get.php @@ -0,0 +1,46 @@ + $offset + * @return T + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + + } + + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + + } + + #[\ReturnTypeWillChange] + public function offsetUnset($offset) + { + + } + +} + +function (Foo $foo): void { + assertType(stdClass::class, $foo->offsetGet(stdClass::class)); + assertType(stdClass::class, $foo[stdClass::class]); +}; diff --git a/tests/PHPStan/Analyser/data/generic-unions.php b/tests/PHPStan/Analyser/data/generic-unions.php index 7ebc5bb89b..4fe7aac9f5 100644 --- a/tests/PHPStan/Analyser/data/generic-unions.php +++ b/tests/PHPStan/Analyser/data/generic-unions.php @@ -61,3 +61,75 @@ public function foo( } } + +class InvokableClass +{ + public function __invoke(): string + { + return 'foo'; + } +} + +/** + * + * @template TGetDefault + * @template TKey + * + * @param TKey $key + * @param TGetDefault|(\Closure(): TGetDefault) $default + * @return TKey|TGetDefault + */ +function getWithDefault($key, $default = null) +{ + if(rand(0,10) > 5) { + return $key; + } + + if (is_callable($default)) { + return $default(); + } + + return $default; +} + +/** + * + * @template TGetDefault + * @template TKey + * + * @param TKey $key + * @param TGetDefault|(callable(): TGetDefault) $default + * @return TKey|TGetDefault + */ +function getWithDefaultCallable($key, $default = null) +{ + if(rand(0,10) > 5) { + return $key; + } + + if (is_callable($default)) { + return $default(); + } + + 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 () { + return 'foo'; +})); +assertType('int|string', getWithDefaultCallable(3, function () { + return 'foo'; +})); +assertType('GenericUnions\Foo|int', getWithDefault(3, function () { + return new Foo; +})); +assertType('GenericUnions\Foo|int', 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)); diff --git a/tests/PHPStan/Analyser/data/generics-empty-array.php b/tests/PHPStan/Analyser/data/generics-empty-array.php new file mode 100644 index 0000000000..b238f3fdbf --- /dev/null +++ b/tests/PHPStan/Analyser/data/generics-empty-array.php @@ -0,0 +1,80 @@ + $a + * @return array{TKey, T} + */ + public function doFoo(array $a = []): array + { + + } + + public function doBar() + { + assertType('array{*NEVER*, *NEVER*}', $this->doFoo()); + assertType('array{*NEVER*, *NEVER*}', $this->doFoo([])); + } + +} + +/** + * @template TKey of array-key + * @template T + */ +class ArrayCollection +{ + + /** + * @param array $items + */ + public function __construct(array $items = []) + { + + } + +} + +class Bar +{ + + public function doFoo() + { + assertType('GenericsEmptyArray\\ArrayCollection<*NEVER*, *NEVER*>', new ArrayCollection()); + assertType('GenericsEmptyArray\\ArrayCollection<*NEVER*, *NEVER*>', new ArrayCollection([])); + } + +} + +/** + * @template TKey of array-key + * @template T + */ +class ArrayCollection2 +{ + + public function __construct(array $items = []) + { + + } + +} + +class Baz +{ + + public function doFoo() + { + assertType('GenericsEmptyArray\\ArrayCollection2<(int|string), mixed>', new ArrayCollection2()); + assertType('GenericsEmptyArray\\ArrayCollection2<(int|string), mixed>', new ArrayCollection2([])); + } + +} diff --git a/tests/PHPStan/Analyser/data/generics.php b/tests/PHPStan/Analyser/data/generics.php index 17a5b9e3ca..45e522e954 100644 --- a/tests/PHPStan/Analyser/data/generics.php +++ b/tests/PHPStan/Analyser/data/generics.php @@ -96,8 +96,8 @@ function testD($int, $float, $intFloat) assertType('float|int', d($int, $float)); 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{}|DateTime', d([], new \DateTime())); + assertType('array{blabla: string}|DateTime', d(['blabla' => 'barrrr'], new \DateTime())); } /** @@ -147,9 +147,15 @@ function f($a, $b) */ function testF($arrayOfInt, $callableOrNull) { - assertType('array', f($arrayOfInt, function (int $a): string { + assertType('Closure(int): numeric-string', function (int $a): string { + return (string)$a; + }); + assertType('array', f($arrayOfInt, function (int $a): string { return (string)$a; })); + assertType('Closure(mixed): string', function ($a): string { + return (string)$a; + }); assertType('array', f($arrayOfInt, function ($a): string { return (string)$a; })); @@ -218,7 +224,7 @@ function testArrayMap(array $listOfIntegers) return (string) $int; }, $listOfIntegers); - assertType('array', $strings); + assertType('array', $strings); } /** @@ -341,7 +347,7 @@ function varAnnotation($cb) /** @var T */ $v = $cb(); - assertType('T (function PHPStan\Generics\FunctionsAssertType\varAnnotation(), argument)', $v); + assertType('T (function PHPStan\Generics\FunctionsAssertType\varAnnotation(), parameter)', $v); return $v; } @@ -365,7 +371,7 @@ public function f($p, $cb) /** @var T */ $v = $cb(); - assertType('T (class PHPStan\Generics\FunctionsAssertType\C, argument)', $v); + assertType('T (class PHPStan\Generics\FunctionsAssertType\C, parameter)', $v); // should be argument assertType('T (class PHPStan\Generics\FunctionsAssertType\C, argument)', $this->a); @@ -378,7 +384,7 @@ public function g() } }; - assertType('T (class PHPStan\Generics\FunctionsAssertType\C, argument)', $a->g()); + assertType('T (class PHPStan\Generics\FunctionsAssertType\C, parameter)', $a->g()); } } @@ -757,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, string, PHPStan\\Generics\\FunctionsAssertType\\A}', $factory->create(new \DateTime(), '', new A(new \DateTime())) ); } @@ -924,8 +930,8 @@ public function returnStatic(): self function () { $stdEmpty = new StdClassCollection([]); - assertType('PHPStan\Generics\FunctionsAssertType\StdClassCollection<(int|string), stdClass>', $stdEmpty); - assertType('array', $stdEmpty->getAll()); + assertType('PHPStan\Generics\FunctionsAssertType\StdClassCollection<*NEVER*, *NEVER*>', $stdEmpty); + assertType('array{}', $stdEmpty->getAll()); $std = new StdClassCollection([new \stdClass()]); assertType('PHPStan\Generics\FunctionsAssertType\StdClassCollection', $std); @@ -946,7 +952,7 @@ public function doFoo($a) /** @var T $b */ $b = doFoo(); - assertType('T (method PHPStan\Generics\FunctionsAssertType\ClassWithMethodCachingIssue::doFoo(), argument)', $b); + assertType('T (method PHPStan\Generics\FunctionsAssertType\ClassWithMethodCachingIssue::doFoo(), parameter)', $b); } /** @@ -959,7 +965,7 @@ public function doBar($a) /** @var T $b */ $b = doFoo(); - assertType('T (method PHPStan\Generics\FunctionsAssertType\ClassWithMethodCachingIssue::doBar(), argument)', $b); + assertType('T (method PHPStan\Generics\FunctionsAssertType\ClassWithMethodCachingIssue::doBar(), parameter)', $b); } } @@ -1028,8 +1034,8 @@ class StaticClassConstant public function doFoo() { $staticClassName = static::class; - assertType('class-string', $staticClassName); - assertType('static(PHPStan\Generics\FunctionsAssertType\StaticClassConstant)', new $staticClassName); + assertType('class-string)>', $staticClassName); + assertType('static(PHPStan\Generics\FunctionsAssertType\StaticClassConstant)', new $staticClassName); } /** @@ -1083,7 +1089,7 @@ function testGenericObjectWithoutClassType2($a) return $a; } - assertType('T of object (function PHPStan\Generics\FunctionsAssertType\testGenericObjectWithoutClassType2(), argument)', $b); + assertType('T of object~stdClass (function PHPStan\Generics\FunctionsAssertType\testGenericObjectWithoutClassType2(), argument)', $b); return $a; } @@ -1102,6 +1108,7 @@ function () { class GenericReflectionClass extends \ReflectionClass { + #[\ReturnTypeWillChange] public function newInstanceWithoutConstructor() { return parent::newInstanceWithoutConstructor(); @@ -1115,6 +1122,7 @@ public function newInstanceWithoutConstructor() class SpecificReflectionClass extends \ReflectionClass { + #[\ReturnTypeWillChange] public function newInstanceWithoutConstructor() { return parent::newInstanceWithoutConstructor(); @@ -1293,7 +1301,7 @@ function arrayOfGenericClassStrings(array $a): void function getClassOnTemplateType($a, $b, $c, $d, $object, $mixed, $tObject) { assertType( - 'class-string', + 'class-string|false', get_class($a) ); assertType( @@ -1318,7 +1326,7 @@ function getClassOnTemplateType($a, $b, $c, $d, $object, $mixed, $tObject) ); assertType('class-string', get_class($object)); - assertType('class-string', get_class($mixed)); + assertType('class-string|false', get_class($mixed)); assertType('class-string', get_class($tObject)); } @@ -1392,10 +1400,165 @@ public function process($class): void { } function (\Throwable $e): void { - assertType('mixed', $e->getCode()); + assertType('(int|string)', $e->getCode()); }; function (): void { $array = ['a' => 1, 'b' => 2]; - assertType('array(\'a\' => int, \'b\' => int)', a($array)); + assertType('array{a: int, b: int}', a($array)); +}; + + +/** + * @template T of bool + * @param T $b + * @return T + */ +function boolBound(bool $b): bool +{ + return $b; +} + +function (bool $b): void { + assertType('true', boolBound(true)); + assertType('false', boolBound(false)); + assertType('bool', boolBound($b)); +}; + +/** + * @template T of float + * @param T $f + * @return T + */ +function floatBound(float $f): float +{ + return $f; +} + +function (float $f): void { + assertType('1.0', floatBound(1.0)); + assertType('float', floatBound($f)); +}; + +/** + * @template T of string|int|float|bool + */ +class UnionT +{ + + /** + * @param T|null $t + * @return T|null + */ + public function doFoo($t) + { + return $t; + } + +} + +/** + * @param UnionT $foo + */ +function foooo(UnionT $foo): void +{ + assertType('string|null', $foo->doFoo('a')); +} + +/** + * @template T1 of object + * @param T1 $type + * @return T1 + */ +function newObject($type): void +{ + assertType('T1 of object (function PHPStan\Generics\FunctionsAssertType\newObject(), argument)', new $type); +} + +function newStdClass(\stdClass $std): void +{ + assertType('stdClass', new $std); +} + +/** + * @template T1 of object + * @param class-string $type + * @return T1 + */ +function newClassString($type): void +{ + assertType('T1 of object (function PHPStan\Generics\FunctionsAssertType\newClassString(), argument)', new $type); +} + +/** + * @template T of array + * @param T $a + * @return T + */ +function arrayBound1(array $a): array +{ + return $a; +} + +/** + * @template T of array + * @param T $a + * @return T + */ +function arrayBound2(array $a): array +{ + return $a; +} + +/** + * @template T of list + * @param T $a + * @return T + */ +function arrayBound3(array $a): array +{ + return $a; +} + +/** + * @template T of list> + * @param T $a + * @return T + */ +function arrayBound4(array $a): array +{ + return $a; +} + +/** + * @template T of array + * @param T $a + * @return array + */ +function arrayBound5(array $a): array +{ + return $a; +} + +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{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'))); +}; + +/** + * @template T of array{0: string, 1: bool} + * @param T $a + * @return T + */ +function constantArrayBound(array $a): array +{ + return $a; +} + +function (): void { + assertType('array{\'string\', true}', constantArrayBound(['string', true])); }; diff --git a/tests/PHPStan/Analyser/data/hash-functions-74.php b/tests/PHPStan/Analyser/data/hash-functions-74.php new file mode 100644 index 0000000000..85915248d7 --- /dev/null +++ b/tests/PHPStan/Analyser/data/hash-functions-74.php @@ -0,0 +1,53 @@ + 0 && $a['a'] === $a['b']['c']) { + assertType('array{a: non-empty-string, b: array{c: non-empty-string}}', $a); + } + } + +} + +class Bar +{ + + public function doFoo(\stdClass $a, \stdClass $b): void + { + assertType('true', $a === $a); + assertType('bool', $a === $b); + assertType('false', $a !== $a); + assertType('bool', $a !== $b); + + assertType('bool', self::createStdClass() === self::createStdClass()); + assertType('bool', self::createStdClass() !== self::createStdClass()); + } + + public static function createStdClass(): \stdClass + { + + } + +} diff --git a/tests/PHPStan/Analyser/data/if-defined.php b/tests/PHPStan/Analyser/data/if-defined.php index 81832db199..f0523beb8b 100644 --- a/tests/PHPStan/Analyser/data/if-defined.php +++ b/tests/PHPStan/Analyser/data/if-defined.php @@ -5,21 +5,25 @@ class Foo implements \ArrayAccess { + #[\ReturnTypeWillChange] public function offsetExists($offset) { } + #[\ReturnTypeWillChange] public function offsetGet($offset) { } + #[\ReturnTypeWillChange] public function offsetSet($offset, $value) { } + #[\ReturnTypeWillChange] public function offsetUnset($offset) { diff --git a/tests/PHPStan/Analyser/data/implode.php b/tests/PHPStan/Analyser/data/implode.php new file mode 100644 index 0000000000..b2e2b9aa7c --- /dev/null +++ b/tests/PHPStan/Analyser/data/implode.php @@ -0,0 +1,24 @@ +', $i); // should improved to be int<0, 4> + assertType('int<0, 4>', $i); } $i = 0; while ($i < 5) { - assertType('int', $i); // should improved to be int<0, 4> + assertType('int<0, 4>', $i); $i++; } $i = 0; while ($i++ < 5) { - assertType('int', $i); // should improved to be int<1, 5> + assertType('int<1, 5>', $i); } $i = 0; while (++$i < 5) { - assertType('int', $i); // should improved to be int<1, 4> + assertType('int<1, 4>', $i); } $i = 5; while ($i-- > 0) { - assertType('int<0, max>', $i); // should improved to be int<0, 4> + assertType('int<0, 4>', $i); } $i = 5; while (--$i > 0) { - assertType('int<1, max>', $i); // should improved to be int<1, 4> + assertType('int<1, 4>', $i); } }; @@ -152,7 +152,182 @@ function (int $a, int $b, int $c): void { assertType('int<14, max>', $c); - assertType('int', $a * $b); - assertType('int', $b * $c); - assertType('int', $a * $b * $c); + assertType('int<156, max>', $a * $b); + assertType('int<182, max>', $b * $c); + assertType('int<2184, max>', $a * $b * $c); }; + +class X { + /** + * @var int<0, 100> + */ + public $percentage; + /** + * @var int + */ + public $min; + /** + * @var int<0, max> + */ + public $max; + + /** + * @var int<0, something> + */ + public $error1; + /** + * @var int + */ + public $error2; + + /** + * @var int + */ + public $int; + + public function supportsPhpdocIntegerRange() { + assertType('int<0, 100>', $this->percentage); + assertType('int', $this->min); + assertType('int<0, max>', $this->max); + + assertType('*ERROR*', $this->error1); + assertType('*ERROR*', $this->error2); + assertType('int', $this->int); + } + + /** + * @param int $i + * @param 1|2|3 $j + * @param 1|-20|3 $z + * @param positive-int $pi + * @param int<1, 10> $r1 + * @param int<5, 10> $r2 + * @param int<-9, 100> $r3 + * @param int $rMin + * @param int<5, max> $rMax + * + * @param 20|40|60 $x + * @param 2|4 $y + */ + public function math($i, $j, $z, $pi, $r1, $r2, $r3, $rMin, $rMax, $x, $y) { + assertType('int', $r1 + $i); + assertType('int', $r1 - $i); + assertType('int', $r1 * $i); + assertType('(float|int)', $r1 / $i); + + assertType('int<2, 13>', $r1 + $j); + assertType('int<-2, 9>', $r1 - $j); + assertType('int<1, 30>', $r1 * $j); + assertType('float|int<0, 10>', $r1 / $j); + assertType('int', $rMin * $j); + assertType('int<5, max>', $rMax * $j); + + assertType('int<2, 13>', $j + $r1); + assertType('int<-9, 2>', $j - $r1); + assertType('int<1, 30>', $j * $r1); + assertType('float|int<0, 3>', $j / $r1); + assertType('int', $j * $rMin); + assertType('int<5, max>', $j * $rMax); + + assertType('int<-19, -10>|int<2, 13>', $r1 + $z); + assertType('int<-2, 9>|int<21, 30>', $r1 - $z); + assertType('int<-200, -20>|int<1, 30>', $r1 * $z); + assertType('float|int<0, 10>', $r1 / $z); + assertType('int', $rMin * $z); + assertType('int|int<5, max>', $rMax * $z); + + assertType('int<2, max>', $pi + 1); + assertType('int<-1, max>', $pi - 2); + assertType('int<2, max>', $pi * 2); + assertType('float|int<0, max>', $pi / 2); + assertType('int<2, max>', 1 + $pi); + assertType('int', 2 - $pi); + assertType('int<2, max>', 2 * $pi); + assertType('float|int<2, max>', 2 / $pi); + + assertType('int<5, 14>', $r1 + 4); + assertType('int<-3, 6>', $r1 - 4); + assertType('int<4, 40>', $r1 * 4); + assertType('float|int<0, 2>', $r1 / 4); + assertType('int<9, max>', $rMax + 4); + assertType('int<1, max>', $rMax - 4); + assertType('int<20, max>', $rMax * 4); + assertType('float|int<1, max>', $rMax / 4); + + assertType('int<6, 20>', $r1 + $r2); + assertType('int<-9, 5>', $r1 - $r2); + assertType('int<5, 100>', $r1 * $r2); + assertType('float|int<0, 1>', $r1 / $r2); + + assertType('int<-99, 19>', $r1 - $r3); + + assertType('int', $r1 + $rMin); + assertType('int<-4, max>', $r1 - $rMin); + assertType('int', $r1 * $rMin); + assertType('float|int', $r1 / $rMin); + assertType('int', $rMin + $r1); + assertType('int', $rMin - $r1); + assertType('int', $rMin * $r1); + assertType('float|int', $rMin / $r1); + + assertType('int<6, max>', $r1 + $rMax); + assertType('int', $r1 - $rMax); + assertType('int<5, max>', $r1 * $rMax); + assertType('float|int<0, max>', $r1 / $rMax); + assertType('int<6, max>', $rMax + $r1); + assertType('int<-5, max>', $rMax - $r1); + assertType('int<5, max>', $rMax * $r1); + assertType('float|int<5, max>', $rMax / $r1); + + assertType('5|10|15|20|30', $x / $y); + + assertType('float|int<0, max>', $rMax / $rMax); + assertType('(float|int)', $rMin / $rMin); + } + + /** + * @param int<0, max> $a + * @param int<0, max> $b + */ + function divisionLoosesInformation(int $a, int $b): void { + assertType('float|int<0, max>',$a/$b); + } + + /** + * @param int $rMin + * @param int<5, max> $rMax + * + * @see https://www.wolframalpha.com/input/?i=%28interval%5B2%2C%E2%88%9E%5D+%2F+-1%29 + * @see https://3v4l.org/ur9Wf + */ + public function maximaInversion($rMin, $rMax) { + assertType('int<-5, max>', -1 * $rMin); + assertType('int', -2 * $rMax); + + assertType('int<-5, max>', $rMin * -1); + assertType('int', $rMax * -2); + + assertType('float|int<0, max>', -1 / $rMin); + assertType('float|int', -2 / $rMax); + + assertType('float|int<-5, max>', $rMin / -1); + assertType('float|int', $rMax / -2); + } + + /** + * @param int<1, 10> $r1 + * @param int<-5, 10> $r2 + * @param int $rMin + * @param int<5, max> $rMax + * @param int<0, 50> $rZero + */ + public function unaryMinus($r1, $r2, $rMin, $rMax, $rZero) { + + assertType('int<-10, -1>', -$r1); + assertType('int<-10, 5>', -$r2); + assertType('int<-5, max>', -$rMin); + assertType('int', -$rMax); + assertType('int<-50, 0>', -$rZero); + } + +} diff --git a/tests/PHPStan/Analyser/data/invalidate-readonly-properties.php b/tests/PHPStan/Analyser/data/invalidate-readonly-properties.php new file mode 100644 index 0000000000..5eb178a8a8 --- /dev/null +++ b/tests/PHPStan/Analyser/data/invalidate-readonly-properties.php @@ -0,0 +1,31 @@ += 8.1 + +namespace InvalidateReadonlyProperties; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + private readonly int $foo; + + public function __construct(int $foo) + { + $this->foo = $foo; + } + + public function doFoo(): void + { + if ($this->foo === 1) { + assertType('1', $this->foo); + $this->doBar(); + assertType('1', $this->foo); + } + } + + public function doBar(): void + { + + } + +} diff --git a/tests/PHPStan/Analyser/data/is-a.php b/tests/PHPStan/Analyser/data/is-a.php index 27bde886c0..8db2aada66 100644 --- a/tests/PHPStan/Analyser/data/is-a.php +++ b/tests/PHPStan/Analyser/data/is-a.php @@ -1,8 +1,10 @@ ', $foo); + \PHPStan\Testing\assertType('class-string', $foo); } }; function (string $foo, string $someString) { if (is_a($foo, $someString, true)) { - \PHPStan\Testing\assertType('class-string', $foo); + \PHPStan\Testing\assertType('class-string', $foo); } }; + +function (Bar $a, Bar $b, Bar $c, Bar $d) { + if (is_a($a, Bar::class)) { + \PHPStan\Testing\assertType('IsA\Bar', $a); + } + + if (is_a($b, Foo::class)) { + \PHPStan\Testing\assertType('IsA\Bar', $b); + } + + /** @var class-string $barClassString */ + $barClassString = 'Bar'; + if (is_a($c, $barClassString)) { + \PHPStan\Testing\assertType('IsA\Bar', $c); + } + + /** @var class-string $fooClassString */ + $fooClassString = 'Foo'; + if (is_a($d, $fooClassString)) { + \PHPStan\Testing\assertType('IsA\Bar', $d); + } +}; + +function (string $a, string $b, string $c, string $d) { + /** @var class-string $a */ + if (is_a($a, Bar::class, true)) { + \PHPStan\Testing\assertType('class-string', $a); + } + + /** @var class-string $b */ + if (is_a($b, Foo::class, true)) { + \PHPStan\Testing\assertType('class-string', $b); + } + + /** @var class-string $c */ + /** @var class-string $barClassString */ + $barClassString = 'Bar'; + if (is_a($c, $barClassString, true)) { + \PHPStan\Testing\assertType('class-string', $c); + } + + /** @var class-string $d */ + /** @var class-string $fooClassString */ + $fooClassString = 'Foo'; + if (is_a($d, $fooClassString, true)) { + \PHPStan\Testing\assertType('class-string', $d); + } +}; + +class Foo {} + +class Bar extends Foo {} diff --git a/tests/PHPStan/Analyser/data/is-subclass-of.php b/tests/PHPStan/Analyser/data/is-subclass-of.php new file mode 100644 index 0000000000..1469236ea5 --- /dev/null +++ b/tests/PHPStan/Analyser/data/is-subclass-of.php @@ -0,0 +1,55 @@ + $barClassString */ + $barClassString = 'Bar'; + if (is_subclass_of($c, $barClassString)) { + \PHPStan\Testing\assertType('IsSubclassOf\Bar', $c); + } + + /** @var class-string $fooClassString */ + $fooClassString = 'Foo'; + if (is_subclass_of($d, $fooClassString)) { + \PHPStan\Testing\assertType('IsSubclassOf\Bar', $d); + } +}; + +function (string $a, string $b, string $c, string $d) { + /** @var class-string $a */ + if (is_subclass_of($a, Bar::class)) { + \PHPStan\Testing\assertType('class-string', $a); + } + + /** @var class-string $b */ + if (is_subclass_of($b, Foo::class)) { + \PHPStan\Testing\assertType('class-string', $b); + } + + /** @var class-string $c */ + /** @var class-string $barClassString */ + $barClassString = 'Bar'; + if (is_subclass_of($c, $barClassString)) { + \PHPStan\Testing\assertType('class-string', $c); + } + + /** @var class-string $d */ + /** @var class-string $fooClassString */ + $fooClassString = 'Foo'; + if (is_subclass_of($d, $fooClassString)) { + \PHPStan\Testing\assertType('class-string', $d); + } +}; + +class Foo {} + +class Bar extends Foo {} diff --git a/tests/PHPStan/Analyser/data/isset-coalesce-empty-type-post-81.php b/tests/PHPStan/Analyser/data/isset-coalesce-empty-type-post-81.php new file mode 100644 index 0000000000..405b2a7a10 --- /dev/null +++ b/tests/PHPStan/Analyser/data/isset-coalesce-empty-type-post-81.php @@ -0,0 +1,7 @@ +|false', $ref->name ?? false); +} diff --git a/tests/PHPStan/Analyser/data/isset-coalesce-empty-type-pre-81.php b/tests/PHPStan/Analyser/data/isset-coalesce-empty-type-pre-81.php new file mode 100644 index 0000000000..d27bde20f6 --- /dev/null +++ b/tests/PHPStan/Analyser/data/isset-coalesce-empty-type-pre-81.php @@ -0,0 +1,7 @@ +', $ref->name ?? false); +} diff --git a/tests/PHPStan/Analyser/data/isset-coalesce-empty-type-root.php b/tests/PHPStan/Analyser/data/isset-coalesce-empty-type-root.php new file mode 100644 index 0000000000..29671650c0 --- /dev/null +++ b/tests/PHPStan/Analyser/data/isset-coalesce-empty-type-root.php @@ -0,0 +1,19 @@ += 7.4 + +namespace IssetCoalesceEmptyType; + +use function PHPStan\Testing\assertType; + +class FooEmpty +{ + + public function doFoo() + { + $a = []; + if (rand(0, 1)) { + $a[0] = rand(0,1) ? true : false; + $a[1] = false; + } + + $a[2] = rand(0,1) ? true : false; + $a[3] = false; + $a[4] = true; + + assertType('bool', empty($a[0])); + assertType('bool', empty($a[1])); + assertType('true', empty($a['nonexistent'])); + assertType('bool', empty($a[2])); + assertType('true', empty($a[3])); + assertType('false', empty($a[4])); + } + + public function doBar() + { + $a = [ + '', + '0', + 'foo', + rand(0, 1) ? '' : 'foo', + ]; + assertType('true', empty($a[0])); + assertType('true', empty($a[1])); + assertType('false', empty($a[2])); + assertType('bool', empty($a[3])); + } + + public function doBaz() + { + assertType('true', empty($a)); + + $b = 'test'; + assertType('false', empty($b)); + + if (rand(0, 1)) { + $c = 'foo'; + } + + assertType('bool', empty($c)); + + $d = rand(0, 1) ? '' : 'foo'; + assertType('bool', empty($d)); + } + +} + +class FooIsset +{ + /** @var string|null */ + public static $staticStringOrNull = null; + + /** @var string */ + public static $staticString = ''; + + /** @var null */ + public static $staticAlwaysNull; + + /** @var string|null */ + public $stringOrNull = null; + + /** @var string */ + public $string = ''; + + /** @var null */ + public $alwaysNull; + + /** @var FooIsset|null */ + public $FooIssetOrNull; + + /** @var FooIsset */ + public $FooIsset; + + public function thisCoalesce() { + assertType('true', isset($this->string)); + } + + function coalesce() + { + + $scalar = 3; + + assertType('true', isset($scalar)); + + $array = [1, 2, 3]; + + assertType('false', isset($array['string'])); + + $multiDimArray = [[1], [2], [3]]; + + assertType('false', isset($multiDimArray['string'])); + + assertType('false', isset($doesNotExist)); + + if (rand() > 0.5) { + $maybeVariable = 3; + } + + assertType('bool', isset($maybeVariable)); + + $fixedDimArray = [ + 'dim' => 1, + 'dim-null' => rand() > 0.5 ? null : 1, + 'dim-null-offset' => ['a' => rand() > 0.5 ? true : null], + 'dim-empty' => [] + ]; + + // Always set + assertType('true', isset($fixedDimArray['dim'])); + + // Maybe set + assertType('bool', isset($fixedDimArray['dim-null'])); + + // Never set, then unknown + assertType('false', isset($fixedDimArray['dim-null-not-set']['a'])); + + // Always set, then always set + assertType('bool', isset($fixedDimArray['dim-null-offset']['a'])); + + // Always set, then never set + assertType('false', isset($fixedDimArray['dim-empty']['b'])); + + $foo = new FooIsset(); + + assertType('bool', isset($foo->stringOrNull)); + + assertType('true', isset($foo->string)); + + assertType('false', isset($foo->alwaysNull)); + + assertType('true', isset($foo->FooIsset->string)); + + assertType('bool', isset($foo->FooIssetOrNull->string)); + + assertType('bool', isset(FooIsset::$staticStringOrNull)); + + assertType('true', isset(FooIsset::$staticString)); + + assertType('false', isset(FooIsset::$staticAlwaysNull)); + } + + /** + * @param array $array + */ + function coalesceStringOffset(array $array) + { + assertType('bool', isset($array['string'])); + } + + function alwaysNullCoalesce (?string $a): void + { + if (!is_string($a)) { + assertType('false', isset($a)); + } + } + + function fooo(): void { + assertType('true', isset((new FooIsset())->string)); + assertType('bool', isset((new FooIsset())->stringOrNull)); + assertType('false', isset((new FooIsset())->alwaysNull)); + } + + function fooooo(FooIsset $foo): void + { + assertType('false', isset($foo::$staticAlwaysNull)); + assertType('true', isset($foo::$staticString)); + assertType('bool', isset($foo::$staticStringOrNull)); + } +} + +/** + * @property int $integerProperty + * @property FooIsset $foo + */ +class SomeMagicProperties +{ + + function doFoo(SomeMagicProperties $foo, \stdClass $std): void { + assertType('bool', isset($foo->integerProperty)); + + assertType('bool', isset($foo->foo->string)); + + assertType('bool', isset($std->foo)); + } + + function numericStringOffset(string $code): string + { + $array = [1, 2, 3]; + assertType('bool', isset($array[$code])); + + if (isset($array[$code])) { + return (string) $array[$code]; + } + + $mappings = [ + '21021200' => '21028800', + ]; + + assertType('bool', isset($mappings[$code])); + + if (isset($mappings[$code])) { + return (string) $mappings[$code]; + } + + throw new \RuntimeException(); + } + + + /** + * @param array{foo: string} $array + * @param 'bar' $bar + */ + function offsetFromPhpdoc(array $array, string $bar) + { + assertType('true', isset($array['foo'])); + + $array = ['bar' => 1]; + assertType('true', isset($array[$bar])); + } + + +} + +class FooNativeProp +{ + + public int $hasDefaultValue = 0; + + public int $isAssignedBefore; + + public int $canBeUninitialized; + + function doFoo(FooNativeProp $foo): void { + assertType('bool', isset($foo->hasDefaultValue)); + + $foo->isAssignedBefore = 5; + assertType('true', isset($foo->isAssignedBefore)); + + assertType('bool', isset($foo->canBeUninitialized)); + } + +} + +class Bug4290Isset +{ + public function test(): void + { + $array = self::getArray(); + + assertType('bool', isset($array['status'])); + assertType('bool', isset($array['value'])); + + $data = array_filter([ + 'status' => isset($array['status']) ? $array['status'] : null, + 'value' => isset($array['value']) ? $array['value'] : null, + ]); + + if (count($data) === 0) { + return; + } + + assertType('bool', isset($data['status'])); + + isset($data['status']) ? 1 : 0; + } + + /** + * @return string[] + */ + public static function getArray(): array + { + return ['value' => '100']; + } +} + +class Bug4671 +{ + + /** + * @param array $strings + */ + public function doFoo(int $intput, array $strings): void + { + assertType('false', isset($strings[(string) $intput])); + } + +} + +class MoreIsset +{ + + function one() + { + + /** @var string|null $alwaysDefinedNullable */ + $alwaysDefinedNullable = doFoo(); + + assertType('bool', isset($alwaysDefinedNullable)); + + $alwaysDefinedNotNullable = 'string'; + assertType('true', isset($alwaysDefinedNotNullable)); + + if (doFoo()) { + $sometimesDefinedVariable = 1; + } + + assertType('bool', isset( + $sometimesDefinedVariable // fine, this is what's isset() is for + )); + + assertType('false', isset( + $sometimesDefinedVariable, // fine, this is what's isset() is for + $neverDefinedVariable // always false + )); + + assertType('false', isset( + $neverDefinedVariable // always false + )); + + /** @var array|null $anotherAlwaysDefinedNullable */ + $anotherAlwaysDefinedNullable = doFoo(); + + assertType('bool', isset($anotherAlwaysDefinedNullable['test']['test'])); + + /** @var array $anotherAlwaysDefinedNotNullable */ + $anotherAlwaysDefinedNotNullable = doFoo(); + assertType('bool', isset($anotherAlwaysDefinedNotNullable['test']['test'])); + + assertType('false', isset($anotherNeverDefinedVariable['test']['test']->test['test']['test'])); + + assertType('false', isset($yetAnotherNeverDefinedVariable::$test['test'])); + + assertType('bool', isset($_COOKIE['test'])); + + assertType('false', isset($yetYetAnotherNeverDefinedVariableInIsset)); + + if (doFoo()) { + $yetAnotherVariableThatSometimesExists = 1; + } + + assertType('bool', isset($yetAnotherVariableThatSometimesExists)); + + /** @var string|null $nullableVariableUsedInTernary */ + $nullableVariableUsedInTernary = doFoo(); + assertType('bool', isset($nullableVariableUsedInTernary)); + } + + function two() { + $alwaysDefinedNotNullable = 'string'; + if (doFoo()) { + $sometimesDefinedVariable = 1; + } + + assertType('false', isset( + $alwaysDefinedNotNullable, // always true + $sometimesDefinedVariable, // fine, this is what's isset() is for + $neverDefinedVariable // always false + )); + + assertType('true', isset( + $alwaysDefinedNotNullable // always true + )); + + assertType('bool', isset( + $alwaysDefinedNotNullable, // always true + $sometimesDefinedVariable // fine, this is what's isset() is for + )); + } + + function three() { + $null = null; + + assertType('false', isset($null)); + } + + function four() { + assertType('bool', isset($_SESSION)); + assertType('bool', isset($_SESSION['foo'])); + } + +} + +class FooCoalesce +{ + /** @var string|null */ + public static $staticStringOrNull = null; + + /** @var string */ + public static $staticString = ''; + + /** @var null */ + public static $staticAlwaysNull; + + /** @var string|null */ + public $stringOrNull = null; + + /** @var string */ + public $string = ''; + + /** @var null */ + public $alwaysNull; + + /** @var FooCoalesce|null */ + public $fooCoalesceOrNull; + + /** @var FooCoalesce */ + public $fooCoalesce; + + public function thisCoalesce() { + assertType('string', $this->string ?? false); + } + + function coalesce() + { + + $scalar = 3; + + assertType('3', $scalar ?? 4); + + $array = [1, 2, 3]; + + assertType('0', $array['string'] ?? 0); + + $multiDimArray = [[1], [2], [3]]; + + assertType('0', $multiDimArray['string'] ?? 0); + + assertType('0', $doesNotExist ?? 0); + + if (rand() > 0.5) { + $maybeVariable = 3; + } + + assertType('0|3', $maybeVariable ?? 0); + + $fixedDimArray = [ + 'dim' => 1, + 'dim-null' => rand() > 0.5 ? null : 1, + 'dim-null-offset' => ['a' => rand() > 0.5 ? true : null], + 'dim-empty' => [] + ]; + + // Always set + assertType('1', $fixedDimArray['dim'] ?? 0); + + // Maybe set + assertType('0|1', $fixedDimArray['dim-null'] ?? 0); + + // Never set, then unknown + assertType('0', $fixedDimArray['dim-null-not-set']['a'] ?? 0); + + // Always set, then always set + assertType('0|true', $fixedDimArray['dim-null-offset']['a'] ?? 0); + + // Always set, then never set + assertType('0', $fixedDimArray['dim-empty']['b'] ?? 0); + + assertType('int<0, max>', rand() ?? false); + + assertType('0|string', preg_replace('', '', '') ?? 0); + + $foo = new FooCoalesce(); + + assertType('string|false', $foo->stringOrNull ?? false); + + assertType('string', $foo->string ?? false); + + assertType('\'\'', $foo->alwaysNull ?? ''); + + assertType('string', $foo->fooCoalesce->string ?? false); + + assertType('string|false', $foo->fooCoalesceOrNull->string ?? false); + + assertType('string|false', FooCoalesce::$staticStringOrNull ?? false); + + assertType('string', FooCoalesce::$staticString ?? false); + + assertType('false', FooCoalesce::$staticAlwaysNull ?? false); + } + + /** + * @param array $array + */ + function coalesceStringOffset(array $array) + { + assertType('int|false', $array['string'] ?? false); + } + + function alwaysNullCoalesce (?string $a): void + { + if (!is_string($a)) { + assertType('false', $a ?? false); + } + } + + function foo(): void { + assertType('string', (new FooCoalesce())->string ?? false); + assertType('string|false', (new FooCoalesce())->stringOrNull ?? false); + assertType('false', (new FooCoalesce())->alwaysNull ?? false); + + assertType(FooCoalesce::class, (new FooCoalesce()) ?? false); + assertType('\'foo\'', null ?? 'foo'); + } + + function bar(FooCoalesce $foo): void + { + assertType('false', $foo::$staticAlwaysNull ?? false); + assertType('string', $foo::$staticString ?? false); + assertType('string|false', $foo::$staticStringOrNull ?? false); + } + + function lorem(): void { + assertType('\'foo\'', $foo ?? 'foo'); + assertType('\'foo\'', $bar->bar ?? 'foo'); + } + + function ipsum(): void { + $scalar = 3; + assertType('3', $scalar ?? 4); + assertType('0', $doesNotExist ?? 0); + } + + function ipsum2(?string $a): void { + if (!is_string($a)) { + assertType('\'foo\'', $a ?? 'foo'); + } + } + +} diff --git a/tests/PHPStan/Analyser/data/iterator_to_array.php b/tests/PHPStan/Analyser/data/iterator_to_array.php index 0ef007f922..e72040ad8c 100644 --- a/tests/PHPStan/Analyser/data/iterator_to_array.php +++ b/tests/PHPStan/Analyser/data/iterator_to_array.php @@ -9,10 +9,26 @@ class Foo { /** - * @param Traversable $ints + * @param Traversable $foo */ - public function doFoo(Traversable $ints) + public function testDefaultBehavior(Traversable $foo) { - assertType('array', iterator_to_array($ints)); + assertType('array', iterator_to_array($foo)); + } + + /** + * @param Traversable $foo + */ + public function testExplicitlyPreserveKeys(Traversable $foo) + { + assertType('array', iterator_to_array($foo, true)); + } + + /** + * @param Traversable $foo + */ + public function testNotPreservingKeys(Traversable $foo) + { + assertType('array', iterator_to_array($foo, false)); } } diff --git a/tests/PHPStan/Analyser/data/key-of.php b/tests/PHPStan/Analyser/data/key-of.php new file mode 100644 index 0000000000..f52eb2effd --- /dev/null +++ b/tests/PHPStan/Analyser/data/key-of.php @@ -0,0 +1,41 @@ + 'John F. Kennedy Airport', + self::LGA => 'La Guardia Airport', + ]; + + /** + * @param key-of $code + */ + public static function foo(string $code): void + { + assertType('\'jfk\'|\'lga\'', $code); + } + + /** + * @param key-of<'jfk'> $code + */ + public static function bar(string $code): void + { + assertType('string', $code); + } + + /** + * @param key-of<'jfk'|'lga'> $code + */ + public static function baz(string $code): void + { + assertType('string', $code); + } +} diff --git a/tests/PHPStan/Analyser/data/list-type.php b/tests/PHPStan/Analyser/data/list-type.php index 8fccd9f831..592814aa9f 100644 --- a/tests/PHPStan/Analyser/data/list-type.php +++ b/tests/PHPStan/Analyser/data/list-type.php @@ -37,7 +37,7 @@ public function withoutGenerics(): void $list[] = '1'; $list[] = true; $list[] = new \stdClass(); - assertType('array&nonEmpty', $list); + assertType('non-empty-array', $list); } @@ -48,7 +48,7 @@ public function withMixedType(): void $list[] = '1'; $list[] = true; $list[] = new \stdClass(); - assertType('array&nonEmpty', $list); + assertType('non-empty-array', $list); } public function withObjectType(): void @@ -56,7 +56,7 @@ public function withObjectType(): void /** @var list<\DateTime> $list */ $list = []; $list[] = new \DateTime(); - assertType('array&nonEmpty', $list); + assertType('non-empty-array', $list); } /** @return list */ @@ -66,7 +66,7 @@ public function withScalarGoodContent(): void $list = []; $list[] = '1'; $list[] = true; - assertType('array&nonEmpty', $list); + assertType('non-empty-array', $list); } public function withNumericKey(): void @@ -75,7 +75,7 @@ public function withNumericKey(): void $list = []; $list[] = '1'; $list['1'] = true; - assertType('array&nonEmpty', $list); + assertType('non-empty-array', $list); } public function withFullListFunctionality(): void @@ -91,7 +91,7 @@ public function withFullListFunctionality(): void /** @var list $list2 */ $list2 = []; $list2[2] = '1';//Most likely to create a gap in indexes - assertType('array&nonEmpty', $list2); + assertType('non-empty-array', $list2); } } diff --git a/tests/PHPStan/Analyser/data/literal-string.php b/tests/PHPStan/Analyser/data/literal-string.php new file mode 100644 index 0000000000..c55c34feff --- /dev/null +++ b/tests/PHPStan/Analyser/data/literal-string.php @@ -0,0 +1,56 @@ +', self::MAX_TOTAL_PRODUCTS - count($excluded)); + assertType('int', self::MAX_TOTAL_PRODUCTS - $i); + + $maxOrPlusOne = self::MAX_TOTAL_PRODUCTS; + if (rand(0, 1)) { + $maxOrPlusOne++; + } + + assertType('22|23', $maxOrPlusOne); + assertType('int', $maxOrPlusOne - count($excluded)); + } + + public function doBar(int $notZero): void + { + if ($notZero === 0) { + return; + } + + assertType('int|int<2, max>', $notZero + 1); + } + + /** + * @param int<-5, 5> $rangeFiveBoth + * @param int<-5, max> $rangeFiveLeft + * @param int $rangeFiveRight + */ + public function doBaz(int $rangeFiveBoth, int $rangeFiveLeft, int $rangeFiveRight): void + { + assertType('int<-4, 6>', $rangeFiveBoth + 1); + assertType('int<-4, max>', $rangeFiveLeft + 1); + assertType('int<-6, max>', $rangeFiveLeft - 1); + assertType('int', $rangeFiveRight + 1); + assertType('int', $rangeFiveRight - 1); + + assertType('int', $rangeFiveLeft + $rangeFiveRight); + assertType('int', $rangeFiveLeft - $rangeFiveRight); + + assertType('int', $rangeFiveRight + $rangeFiveLeft); + assertType('int', $rangeFiveRight - $rangeFiveLeft); + + assertType('int<-10, 10>', $rangeFiveBoth + $rangeFiveBoth); + assertType('int<-10, 10>', $rangeFiveBoth - $rangeFiveBoth); + + assertType('int<-10, max>', $rangeFiveBoth + $rangeFiveLeft); + assertType('int', $rangeFiveBoth - $rangeFiveLeft); + + assertType('int', $rangeFiveBoth + $rangeFiveRight); + assertType('int<-10, max>', $rangeFiveBoth - $rangeFiveRight); + + assertType('int<-10, max>', $rangeFiveLeft + $rangeFiveBoth); + assertType('int<-10, max>', $rangeFiveLeft - $rangeFiveBoth); + + assertType('int', $rangeFiveRight + $rangeFiveBoth); + assertType('int', $rangeFiveRight - $rangeFiveBoth); + } + + public function doLorem($a, $b): void + { + $nullsReverse = rand(0, 1) ? 1 : -1; + $comparison = $a <=> $b; + assertType('int<-1, 1>', $comparison); + assertType('-1|1', $nullsReverse); + assertType('int<-1, 1>', $comparison * $nullsReverse); + } + + public function doIpsum(int $newLevel): void + { + $min = min(30, $newLevel); + assertType('int', $min); + $minDivFive = $min / 5; + assertType('float|int', $minDivFive); + $volume = 0x10000000 * $minDivFive; + assertType('float|int', $volume); + } + + public function doDolor(int $i): void + { + $chunks = min(200, $i); + assertType('int', $chunks); + $divThirty = $chunks / 30; + assertType('float|int', $divThirty); + assertType('float|int', $divThirty + 3); + } + + public function doSit(int $i, int $j): void + { + if ($i < 0) { + return; + } + if ($j < 1) { + return; + } + + assertType('int<0, max>', $i); + assertType('int<1, max>', $j); + assertType('int', $i - $j); + } + + /** + * @param int<-5, 5> $range + */ + public function multiplyZero(int $i, float $f, $range): void + { + assertType('0', $i * false); + assertType('0.0', $f * false); + assertType('0', $range * false); + + assertType('0', $i * '0'); + assertType('0.0', $f * '0'); + assertType('0', $range * '0'); + + assertType('0', $i * 0); + assertType('0.0', $f * 0); + assertType('0', $range * 0); + + assertType('0', 0 * $i); + assertType('0.0', 0 * $f); + assertType('0', 0 * $range); + + $i *= 0; + $f *= 0; + $range *= 0; + assertType('0', $i); + assertType('0.0', $f); + assertType('0', $range); + + } + +} diff --git a/tests/PHPStan/Analyser/data/mb_substitute_character-php71.php b/tests/PHPStan/Analyser/data/mb_substitute_character-php71.php new file mode 100644 index 0000000000..0ba0e9ab4e --- /dev/null +++ b/tests/PHPStan/Analyser/data/mb_substitute_character-php71.php @@ -0,0 +1,21 @@ +', mb_substitute_character()); +\PHPStan\Testing\assertType('true', mb_substitute_character('')); +\PHPStan\Testing\assertType('false', mb_substitute_character(null)); +\PHPStan\Testing\assertType('true', mb_substitute_character('none')); +\PHPStan\Testing\assertType('true', mb_substitute_character('long')); +\PHPStan\Testing\assertType('true', mb_substitute_character('entity')); +\PHPStan\Testing\assertType('false', mb_substitute_character('foo')); +\PHPStan\Testing\assertType('true', mb_substitute_character('123')); +\PHPStan\Testing\assertType('true', mb_substitute_character('123.4')); +\PHPStan\Testing\assertType('true', mb_substitute_character(0xFFFD)); +\PHPStan\Testing\assertType('false', mb_substitute_character(0x10FFFF)); +\PHPStan\Testing\assertType('false', mb_substitute_character(-1)); +\PHPStan\Testing\assertType('false', mb_substitute_character(0x110000)); +\PHPStan\Testing\assertType('bool', mb_substitute_character($undefined)); +\PHPStan\Testing\assertType('bool', mb_substitute_character(new stdClass())); +\PHPStan\Testing\assertType('bool', mb_substitute_character(function () {})); +\PHPStan\Testing\assertType('true', mb_substitute_character(rand(0xD800, 0xDFFF))); +\PHPStan\Testing\assertType('bool', mb_substitute_character(rand(0, 0xDFFF))); +\PHPStan\Testing\assertType('bool', mb_substitute_character(rand(0xD800, 0x10FFFF))); diff --git a/tests/PHPStan/Analyser/data/mb_substitute_character-php8.php b/tests/PHPStan/Analyser/data/mb_substitute_character-php8.php new file mode 100644 index 0000000000..b53353bd3a --- /dev/null +++ b/tests/PHPStan/Analyser/data/mb_substitute_character-php8.php @@ -0,0 +1,21 @@ +|int<57344, 1114111>', mb_substitute_character()); +\PHPStan\Testing\assertType('*NEVER*', mb_substitute_character('')); +\PHPStan\Testing\assertType('true', mb_substitute_character(null)); +\PHPStan\Testing\assertType('true', mb_substitute_character('none')); +\PHPStan\Testing\assertType('true', mb_substitute_character('long')); +\PHPStan\Testing\assertType('true', mb_substitute_character('entity')); +\PHPStan\Testing\assertType('*NEVER*', mb_substitute_character('foo')); +\PHPStan\Testing\assertType('*NEVER*', mb_substitute_character('123')); +\PHPStan\Testing\assertType('*NEVER*', mb_substitute_character('123.4')); +\PHPStan\Testing\assertType('true', mb_substitute_character(0xFFFD)); +\PHPStan\Testing\assertType('true', mb_substitute_character(0x10FFFF)); +\PHPStan\Testing\assertType('*NEVER*', mb_substitute_character(-1)); +\PHPStan\Testing\assertType('*NEVER*', mb_substitute_character(0x110000)); +\PHPStan\Testing\assertType('bool', mb_substitute_character($undefined)); +\PHPStan\Testing\assertType('*NEVER*', mb_substitute_character(new stdClass())); +\PHPStan\Testing\assertType('*NEVER*', mb_substitute_character(function () {})); +\PHPStan\Testing\assertType('*NEVER*', mb_substitute_character(rand(0xD800, 0xDFFF))); +\PHPStan\Testing\assertType('bool', mb_substitute_character(rand(0, 0xDFFF))); +\PHPStan\Testing\assertType('bool', mb_substitute_character(rand(0xD800, 0x10FFFF))); diff --git a/tests/PHPStan/Analyser/data/mb_substitute_character.php b/tests/PHPStan/Analyser/data/mb_substitute_character.php new file mode 100644 index 0000000000..9dab962ec5 --- /dev/null +++ b/tests/PHPStan/Analyser/data/mb_substitute_character.php @@ -0,0 +1,21 @@ +|int<57344, 1114111>', mb_substitute_character()); +\PHPStan\Testing\assertType('true', mb_substitute_character('')); +\PHPStan\Testing\assertType('false', mb_substitute_character(null)); +\PHPStan\Testing\assertType('true', mb_substitute_character('none')); +\PHPStan\Testing\assertType('true', mb_substitute_character('long')); +\PHPStan\Testing\assertType('true', mb_substitute_character('entity')); +\PHPStan\Testing\assertType('false', mb_substitute_character('foo')); +\PHPStan\Testing\assertType('true', mb_substitute_character('123')); +\PHPStan\Testing\assertType('true', mb_substitute_character('123.4')); +\PHPStan\Testing\assertType('true', mb_substitute_character(0xFFFD)); +\PHPStan\Testing\assertType('true', mb_substitute_character(0x10FFFF)); +\PHPStan\Testing\assertType('false', mb_substitute_character(-1)); +\PHPStan\Testing\assertType('false', mb_substitute_character(0x110000)); +\PHPStan\Testing\assertType('bool', mb_substitute_character($undefined)); +\PHPStan\Testing\assertType('bool', mb_substitute_character(new stdClass())); +\PHPStan\Testing\assertType('bool', mb_substitute_character(function () {})); +\PHPStan\Testing\assertType('false', mb_substitute_character(rand(0xD800, 0xDFFF))); +\PHPStan\Testing\assertType('bool', mb_substitute_character(rand(0, 0xDFFF))); +\PHPStan\Testing\assertType('bool', mb_substitute_character(rand(0xD800, 0x10FFFF))); diff --git a/tests/PHPStan/Analyser/data/minmax-arrays.php b/tests/PHPStan/Analyser/data/minmax-arrays.php index 762c70be68..cffe5de439 100644 --- a/tests/PHPStan/Analyser/data/minmax-arrays.php +++ b/tests/PHPStan/Analyser/data/minmax-arrays.php @@ -101,20 +101,52 @@ function dummy3(array $ints): void function dummy4(\DateTimeInterface $dateA, ?\DateTimeInterface $dateB): void { - assertType('array(0 => DateTimeInterface, ?1 => DateTimeInterface)', array_filter([$dateA, $dateB])); + assertType('array{0: DateTimeInterface, 1?: DateTimeInterface}', array_filter([$dateA, $dateB])); assertType('DateTimeInterface', min(array_filter([$dateA, $dateB]))); assertType('DateTimeInterface', max(array_filter([$dateA, $dateB]))); - assertType('array(?0 => DateTimeInterface)', array_filter([$dateB])); + assertType('array{0?: DateTimeInterface}', array_filter([$dateB])); assertType('DateTimeInterface|false', min(array_filter([$dateB]))); assertType('DateTimeInterface|false', max(array_filter([$dateB]))); } function dummy5(int $i, int $j): void { - assertType('array(?0 => int|int<1, max>, ?1 => int|int<1, max>)', array_filter([$i, $j])); - assertType('array(1 => true)', array_filter([false, true])); + assertType('array{0?: int|int<1, max>, 1?: int|int<1, max>}', array_filter([$i, $j])); + assertType('array{1: true}', array_filter([false, true])); } function dummy6(string $s, string $t): void { - assertType('array(?0 => string, ?1 => string)', array_filter([$s, $t])); + assertType('array{0?: non-empty-string, 1?: non-empty-string}', array_filter([$s, $t])); +} + +class HelloWorld +{ + public function setRange(int $range): void + { + if ($range < 0) { + return; + } + assertType('int<0, 100>', min($range, 100)); + assertType('int<0, 100>', min(100, $range)); + } + + public function setRange2(int $range): void + { + if ($range > 100) { + return; + } + assertType('int<0, 100>', max($range, 0)); + assertType('int<0, 100>', max(0, $range)); + } + + public function boundRange(): void + { + /** + * @var int<1, 6> $range + */ + $range = getFoo(); + + assertType('int<1, 4>', min($range, 4)); + assertType('int<4, 6>', max(4, $range)); + } } diff --git a/tests/PHPStan/Analyser/data/missing-closure-native-return-typehint.php b/tests/PHPStan/Analyser/data/missing-closure-native-return-typehint.php new file mode 100644 index 0000000000..d516f89f23 --- /dev/null +++ b/tests/PHPStan/Analyser/data/missing-closure-native-return-typehint.php @@ -0,0 +1,55 @@ +', (function (bool $bool) { + if ($bool) { + return; + } else { + yield 1; + } + })()); + \PHPStan\Testing\assertType('1|null', (function (bool $bool) { + if ($bool) { + return; + } else { + return 1; + } + })()); + \PHPStan\Testing\assertType('1', (function (): int { + return 1; + })()); + \PHPStan\Testing\assertType('1|null', (function (bool $bool) { + if ($bool) { + return null; + } else { + return 1; + } + })()); + \PHPStan\Testing\assertType('1', (function (bool $bool) { + if ($bool) { + return 1; + } + })()); + + \PHPStan\Testing\assertType('array{foo: \'bar\'}', (function () { + $array = [ + 'foo' => 'bar', + ]; + + return $array; + })()); + } + +} diff --git a/tests/PHPStan/Analyser/data/model-mixin.php b/tests/PHPStan/Analyser/data/model-mixin.php new file mode 100644 index 0000000000..e6598d2a15 --- /dev/null +++ b/tests/PHPStan/Analyser/data/model-mixin.php @@ -0,0 +1,50 @@ += 8.0 + +namespace ModelMixin; + +use function PHPStan\Testing\assertType; + +/** @mixin Builder */ +class Model +{ + /** @param array $args */ + public static function __callStatic(string $method, array $args): mixed + { + (new self)->$method(...$args); + } +} + +/** @template TModel as Model */ +class Builder +{ + /** @return array */ + public function all() { return []; } +} + +class User extends Model +{ +} + +function (): void { + assertType('array', User::all()); +}; + +class MixedMethod +{ + + public function doFoo(): int + { + return 1; + } + +} + +/** @mixin MixedMethod */ +interface InterfaceWithMixin +{ + +} + +function (InterfaceWithMixin $i): void { + assertType('int', $i->doFoo()); +}; diff --git a/tests/PHPStan/Analyser/data/modulo-operator.php b/tests/PHPStan/Analyser/data/modulo-operator.php new file mode 100644 index 0000000000..a4876e59f1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/modulo-operator.php @@ -0,0 +1,76 @@ + $range + * @param int<0, max> $zeroOrMore + * @param 1|2|3 $intConst + * @param int|int<4, max> $unionRange + * @param int|7 $hybridUnionRange + */ + function doBar(int $i, int $j, $p, $range, $zeroOrMore, $intConst, $unionRange, $hybridUnionRange, $mixed) + { + assertType('int<-1, 1>', $i % 2); + assertType('int<0, 1>', $p % 2); + + assertType('int<-2, 2>', $i % 3); + assertType('int<0, 2>', $p % 3); + + assertType('0|1|2', $intConst % 3); + assertType('int<-2, 2>', $i % $intConst); + assertType('int<0, 2>', $p % $intConst); + + assertType('int<0, 2>', $range % 3); + + assertType('int<-9, 9>', $i % $range); + assertType('int<0, 9>', $p % $range); + + assertType('int', $i % $unionRange); + assertType('int<0, max>', $p % $unionRange); + + assertType('int<-6, 6>', $i % $hybridUnionRange); + assertType('int<0, 6>', $p % $hybridUnionRange); + + assertType('int<0, max>', $zeroOrMore % $mixed); + + if ($i === 0) { + return; + } + + assertType('int', $j % $i); + } + + function moduleOne(int $i, float $f) { + assertType('0', true % '1'); + assertType('0', false % '1'); + assertType('0', null % '1'); + assertType('0', -1 % '1'); + assertType('0', 0 % '1'); + assertType('0', 1 % '1'); + assertType('0', '1' % '1'); + assertType('0', 1.24 % '1'); + + assertType('0', $i % 1.0); + assertType('0', $f % 1.0); + + assertType('0', $i % '1.0'); + assertType('0', $f % '1.0'); + + assertType('0', $i % '1'); + assertType('0', $f % '1'); + + assertType('0', $i % true); + assertType('0', $f % true); + + $i %= '1'; + $f %= '1'; + assertType('0', $i); + assertType('0', $f); + } +} diff --git a/tests/PHPStan/Analyser/data/more-type-strings.php b/tests/PHPStan/Analyser/data/more-type-strings.php new file mode 100644 index 0000000000..0951303e20 --- /dev/null +++ b/tests/PHPStan/Analyser/data/more-type-strings.php @@ -0,0 +1,29 @@ + $genericInterfaceString + * @param trait-string $genericTraitString + */ + public function doFoo( + string $interfaceString, + string $traitString, + string $genericInterfaceString, + string $genericTraitString + ): void + { + assertType('class-string', $interfaceString); + assertType('class-string', $traitString); + assertType('class-string', $genericInterfaceString); + assertType('string', $genericTraitString); + } + +} diff --git a/tests/PHPStan/Analyser/data/native-intersection.php b/tests/PHPStan/Analyser/data/native-intersection.php new file mode 100644 index 0000000000..c7dcb7c71c --- /dev/null +++ b/tests/PHPStan/Analyser/data/native-intersection.php @@ -0,0 +1,29 @@ += 8.1 + +namespace NativeIntersection; + +use function PHPStan\Testing\assertType; + +interface A +{ + +} + +interface B +{ + +} + +class Foo +{ + + private A&B $prop; + + public function doFoo(A&B $ab): A&B + { + assertType('NativeIntersection\A&NativeIntersection\B', $this->prop); + assertType('NativeIntersection\A&NativeIntersection\B', $ab); + assertType('NativeIntersection\A&NativeIntersection\B', $this->doFoo($ab)); + } + +} diff --git a/tests/PHPStan/Analyser/data/native-types.php b/tests/PHPStan/Analyser/data/native-types.php index c24de594fb..86f0ad7cd1 100644 --- a/tests/PHPStan/Analyser/data/native-types.php +++ b/tests/PHPStan/Analyser/data/native-types.php @@ -83,8 +83,8 @@ public function doForeach(array $array): void assertNativeType('array', $array); foreach ($array as $key => $value) { - assertType('array&nonEmpty', $array); - assertNativeType('array&nonEmpty', $array); + assertType('non-empty-array', $array); + assertNativeType('non-empty-array', $array); assertType('string', $key); assertNativeType('(int|string)', $key); @@ -121,11 +121,11 @@ public function doCatch($foo): void */ public function doForeachArrayDestructuring(array $array) { - assertType('array', $array); + assertType('array', $array); assertNativeType('array', $array); foreach ($array as $key => [$i, $s]) { - assertType('array&nonEmpty', $array); - assertNativeType('array&nonEmpty', $array); + assertType('non-empty-array', $array); + assertNativeType('non-empty-array', $array); assertType('string', $key); assertNativeType('(int|string)', $key); @@ -159,7 +159,7 @@ public function doIfElse(\DateTimeInterface $date): void assertNativeType(\DateTimeImmutable::class, $date); } else { assertType('*NEVER*', $date); - assertNativeType('DateTimeInterface~DateTimeImmutable', $date); + assertNativeType('DateTime', $date); } assertType(\DateTimeImmutable::class, $date); diff --git a/tests/PHPStan/Analyser/data/nested-namespaces.php b/tests/PHPStan/Analyser/data/nested-namespaces.php index 0f8bb769f4..ff8818d32c 100644 --- a/tests/PHPStan/Analyser/data/nested-namespaces.php +++ b/tests/PHPStan/Analyser/data/nested-namespaces.php @@ -17,5 +17,21 @@ public function __construct(boo $boo, baz $baz) { $this->boo = $boo; $this->baz = $baz; } + + /** + * @return boo + */ + public function getBoo(): boo + { + return $this->boo; + } + + /** + * @return mixed + */ + public function getBaz() + { + return $this->baz; + } } } diff --git a/tests/PHPStan/Analyser/data/never.php b/tests/PHPStan/Analyser/data/never.php new file mode 100644 index 0000000000..d09728b2f3 --- /dev/null +++ b/tests/PHPStan/Analyser/data/never.php @@ -0,0 +1,29 @@ += 8.1 + +namespace NeverTest; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + public function doFoo(): never + { + exit(); + } + + public function doBar() + { + assertType('*NEVER*', $this->doFoo()); + } + + public function doBaz(?int $i) + { + if ($i === null) { + $this->doFoo(); + } + + assertType('int', $i); + } + +} diff --git a/tests/PHPStan/Analyser/data/new-in-initializers-runtime.php b/tests/PHPStan/Analyser/data/new-in-initializers-runtime.php new file mode 100644 index 0000000000..4fc08b50be --- /dev/null +++ b/tests/PHPStan/Analyser/data/new-in-initializers-runtime.php @@ -0,0 +1,7 @@ += 8.1 + +namespace NewInInitializers; + +use function PHPStan\Testing\assertType; + +assertType('stdClass', TEST_OBJECT_CONSTANT); diff --git a/tests/PHPStan/Analyser/data/new-in-initializers.php b/tests/PHPStan/Analyser/data/new-in-initializers.php new file mode 100644 index 0000000000..091e0ee628 --- /dev/null +++ b/tests/PHPStan/Analyser/data/new-in-initializers.php @@ -0,0 +1,46 @@ += 8.1 + +namespace NewInInitializers; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + /** + * @template T of object + * @param T $test + * @return T + */ + public function doFoo( + object $test = new \stdClass() + ): object + { + return $test; + } + + #[\Test(new \stdClass())] + public function doBar() + { + assertType(\stdClass::class, $this->doFoo()); + assertType('$this(NewInInitializers\Foo)', $this->doFoo($this)); + assertType(Bar::class, $this->doFoo(new Bar())); + } + +} + +class Bar extends Foo +{ + + public function doBar() + { + + } + + public function doBaz() + { + static $o = new \stdClass(); + assertType('mixed', $o); + } + +} diff --git a/tests/PHPStan/Analyser/data/non-empty-array-key-type.php b/tests/PHPStan/Analyser/data/non-empty-array-key-type.php index 90ae50bcd5..479fe40846 100644 --- a/tests/PHPStan/Analyser/data/non-empty-array-key-type.php +++ b/tests/PHPStan/Analyser/data/non-empty-array-key-type.php @@ -15,7 +15,7 @@ public function doFoo(array $items) assertType('array', $items); if (count($items) > 0) { - assertType('array&nonEmpty', $items); + assertType('non-empty-array', $items); foreach ($items as $i => $val) { assertType('(int|string)', $i); assertType('stdClass', $val); diff --git a/tests/PHPStan/Analyser/data/non-empty-array.php b/tests/PHPStan/Analyser/data/non-empty-array.php index fa054e9259..2e3a24f305 100644 --- a/tests/PHPStan/Analyser/data/non-empty-array.php +++ b/tests/PHPStan/Analyser/data/non-empty-array.php @@ -25,13 +25,31 @@ public function doFoo( $invalidList2 ): void { - assertType('array&nonEmpty', $array); - assertType('array&nonEmpty', $list); - assertType('array&nonEmpty', $arrayOfStrings); - assertType('array&nonEmpty', $listOfStd); - assertType('array&nonEmpty', $listOfStd2); + assertType('non-empty-array', $array); + assertType('non-empty-array', $list); + assertType('non-empty-array', $arrayOfStrings); + assertType('non-empty-array', $listOfStd); + assertType('non-empty-array', $listOfStd2); assertType('array', $invalidList); assertType('mixed', $invalidList2); } + /** + * @param non-empty-array $array + * @param non-empty-list $list + * @param non-empty-array $stringArray + */ + public function arrayFunctions($array, $list, $stringArray): void + { + assertType('non-empty-array', array_combine($array, $array)); + assertType('non-empty-array', array_combine($list, $list)); + + assertType('non-empty-array', array_merge($array)); + assertType('non-empty-array', array_merge([], $array)); + assertType('non-empty-array', array_merge($array, [])); + assertType('non-empty-array', array_merge($array, $array)); + + assertType('non-empty-array', array_flip($array)); + assertType('non-empty-array', array_flip($stringArray)); + } } diff --git a/tests/PHPStan/Analyser/data/non-empty-string-replace-functions.php b/tests/PHPStan/Analyser/data/non-empty-string-replace-functions.php new file mode 100644 index 0000000000..f9a763598f --- /dev/null +++ b/tests/PHPStan/Analyser/data/non-empty-string-replace-functions.php @@ -0,0 +1,26 @@ + 0) { + assertType('non-empty-string', $s); + return; + } + + assertType('\'\'', $s); + } + + public function doBar3(string $s): void + { + if (strlen($s) >= 1) { + assertType('non-empty-string', $s); + return; + } + + assertType('\'\'', $s); + } + + public function doFoo5(string $s): void + { + if (0 === strlen($s)) { + return; + } + + assertType('non-empty-string', $s); + } + + public function doBar4(string $s): void + { + if (0 < strlen($s)) { + assertType('non-empty-string', $s); + return; + } + + assertType('\'\'', $s); + } + + public function doBar5(string $s): void + { + if (1 <= strlen($s)) { + assertType('non-empty-string', $s); + return; + } + + assertType('\'\'', $s); + } + + public function doFoo3(string $s): void + { + if ($s) { + assertType('non-empty-string', $s); + } else { + assertType('\'\'|\'0\'', $s); + } + } + + /** + * @param non-empty-string $s + */ + public function doFoo4(string $s): void + { + assertType('non-empty-array', explode($s, 'foo')); + } + + /** + * @param non-empty-string $s + */ + public function doWithNumeric(string $s): void + { + if (!is_numeric($s)) { + return; + } + + assertType('non-empty-string&numeric-string', $s); + } + + public function doEmpty(string $s): void + { + if (empty($s)) { + return; + } + + assertType('non-empty-string', $s); + } + + public function doEmpty2(string $s): void + { + if (!empty($s)) { + assertType('non-empty-string', $s); + } + } + + /** + * @param non-empty-string $nonEmpty + * @param positive-int $positiveInt + * @param 1|2|3 $postiveRange + * @param -1|-2|-3 $negativeRange + */ + public function doSubstr(string $s, $nonEmpty, $positiveInt, $postiveRange, $negativeRange): void + { + assertType('string', substr($s, 5)); + + assertType('string', substr($s, -5)); + assertType('non-empty-string', substr($nonEmpty, -5)); + assertType('non-empty-string', substr($nonEmpty, $negativeRange)); + + assertType('string', substr($s, 0, 5)); + assertType('non-empty-string', substr($nonEmpty, 0, 5)); + assertType('non-empty-string', substr($nonEmpty, 0, $postiveRange)); + + assertType('string', substr($nonEmpty, 0, -5)); + + assertType('string', substr($s, 0, $positiveInt)); + assertType('non-empty-string', substr($nonEmpty, 0, $positiveInt)); + } +} + +class ImplodingStrings +{ + + /** + * @param array $commonStrings + */ + public function doFoo(string $s, array $commonStrings): void + { + assertType('string', implode($s, $commonStrings)); + assertType('string', implode(' ', $commonStrings)); + assertType('string', implode('', $commonStrings)); + assertType('string', implode($commonStrings)); + } + + /** + * @param non-empty-array $nonEmptyArrayWithStrings + */ + public function doFoo2(string $s, array $nonEmptyArrayWithStrings): void + { + assertType('string', implode($s, $nonEmptyArrayWithStrings)); + assertType('string', implode('', $nonEmptyArrayWithStrings)); + assertType('non-empty-string', implode(' ', $nonEmptyArrayWithStrings)); + assertType('string', implode($nonEmptyArrayWithStrings)); + } + + /** + * @param array $arrayWithNonEmptyStrings + */ + public function doFoo3(string $s, array $arrayWithNonEmptyStrings): void + { + assertType('string', implode($s, $arrayWithNonEmptyStrings)); + assertType('string', implode('', $arrayWithNonEmptyStrings)); + assertType('string', implode(' ', $arrayWithNonEmptyStrings)); + assertType('string', implode($arrayWithNonEmptyStrings)); + } + + /** + * @param non-empty-array $nonEmptyArrayWithNonEmptyStrings + */ + public function doFoo4(string $s, array $nonEmptyArrayWithNonEmptyStrings): void + { + assertType('non-empty-string', implode($s, $nonEmptyArrayWithNonEmptyStrings)); + assertType('non-empty-string', implode('', $nonEmptyArrayWithNonEmptyStrings)); + assertType('non-empty-string', implode(' ', $nonEmptyArrayWithNonEmptyStrings)); + assertType('non-empty-string', implode($nonEmptyArrayWithNonEmptyStrings)); + } + + public function sayHello(int $i): void + { + // coming from issue #5291 + $s = array(1, $i); + + assertType('non-empty-string', implode("a", $s)); + } + + /** + * @param non-empty-string $glue + */ + public function nonE($glue, array $a) + { + // coming from issue #5291 + if (empty($a)) { + return "xyz"; + } + + assertType('non-empty-string', implode($glue, $a)); + } + + public function sayHello2(int $i): void + { + // coming from issue #5291 + $s = array(1, $i); + + assertType('non-empty-string', join("a", $s)); + } + + /** + * @param non-empty-string $glue + */ + public function nonE2($glue, array $a) + { + // coming from issue #5291 + if (empty($a)) { + return "xyz"; + } + + assertType('non-empty-string', join($glue, $a)); + } + +} + +class LiteralString +{ + + function x(string $tableName, string $original): void + { + assertType('non-empty-string', "from `$tableName`"); + } + + /** + * @param non-empty-string $nonEmpty + */ + function concat(string $s, string $nonEmpty): void + { + assertType('string', $s . ''); + assertType('non-empty-string', $nonEmpty . ''); + assertType('non-empty-string', $nonEmpty . $s); + } + +} + +class GeneralizeConstantStringType +{ + + /** + * @param array $a + * @param non-empty-string $s + */ + public function doFoo(array $a, string $s): void + { + $a[$s] = 2; + + // there might be non-empty-string that becomes a number instead + assertType('non-empty-array', $a); + } + + /** + * @param array $a + * @param non-empty-string $s + */ + public function doFoo2(array $a, string $s): void + { + $a[''] = 2; + assertType('non-empty-array', $a); + } + +} + +class MoreNonEmptyStringFunctions +{ + + /** + * @param non-empty-string $nonEmpty + * @param '1'|'2'|'5'|'10' $constUnion + */ + public function doFoo(string $s, string $nonEmpty, int $i, bool $bool, $constUnion) + { + assertType('string', addslashes($s)); + assertType('non-empty-string', addslashes($nonEmpty)); + assertType('string', addcslashes($s)); + assertType('non-empty-string', addcslashes($nonEmpty)); + + assertType('string', escapeshellarg($s)); + assertType('non-empty-string', escapeshellarg($nonEmpty)); + assertType('string', escapeshellcmd($s)); + assertType('non-empty-string', escapeshellcmd($nonEmpty)); + + assertType('string', strtoupper($s)); + assertType('non-empty-string', strtoupper($nonEmpty)); + assertType('string', strtolower($s)); + assertType('non-empty-string', strtolower($nonEmpty)); + assertType('string', mb_strtoupper($s)); + assertType('non-empty-string', mb_strtoupper($nonEmpty)); + assertType('string', mb_strtolower($s)); + assertType('non-empty-string', mb_strtolower($nonEmpty)); + assertType('string', lcfirst($s)); + assertType('non-empty-string', lcfirst($nonEmpty)); + assertType('string', ucfirst($s)); + assertType('non-empty-string', ucfirst($nonEmpty)); + assertType('string', ucwords($s)); + assertType('non-empty-string', ucwords($nonEmpty)); + assertType('string', htmlspecialchars($s)); + assertType('non-empty-string', htmlspecialchars($nonEmpty)); + assertType('string', htmlentities($s)); + assertType('non-empty-string', htmlentities($nonEmpty)); + + assertType('string', urlencode($s)); + assertType('non-empty-string', urlencode($nonEmpty)); + assertType('string', urldecode($s)); + assertType('non-empty-string', urldecode($nonEmpty)); + assertType('string', rawurlencode($s)); + assertType('non-empty-string', rawurlencode($nonEmpty)); + assertType('string', rawurldecode($s)); + assertType('non-empty-string', rawurldecode($nonEmpty)); + + assertType('string', preg_quote($s)); + assertType('non-empty-string', preg_quote($nonEmpty)); + + assertType('string', sprintf($s)); + assertType('non-empty-string', sprintf($nonEmpty)); + assertType('string', vsprintf($s, [])); + assertType('non-empty-string', vsprintf($nonEmpty, [])); + + assertType('0', strlen('')); + assertType('5', strlen('hallo')); + assertType('int<0, 1>', strlen($bool)); + assertType('int<1, max>', strlen($i)); + assertType('int<0, max>', strlen($s)); + assertType('int<1, max>', strlen($nonEmpty)); + assertType('int<1, 2>', strlen($constUnion)); + + assertType('non-empty-string', str_pad($nonEmpty, 0)); + assertType('non-empty-string', str_pad($nonEmpty, 1)); + assertType('string', str_pad($s, 0)); + assertType('non-empty-string', str_pad($s, 1)); + + assertType('non-empty-string', str_repeat($nonEmpty, 1)); + assertType('\'\'', str_repeat($nonEmpty, 0)); + assertType('string', str_repeat($nonEmpty, $i)); + assertType('\'\'', str_repeat($s, 0)); + assertType('string', str_repeat($s, 1)); + assertType('string', str_repeat($s, $i)); + } + +} diff --git a/tests/PHPStan/Analyser/data/nullable-closure-parameter.php b/tests/PHPStan/Analyser/data/nullable-closure-parameter.php new file mode 100644 index 0000000000..86eb0c1e87 --- /dev/null +++ b/tests/PHPStan/Analyser/data/nullable-closure-parameter.php @@ -0,0 +1,24 @@ += 7.4 + +namespace NullableClosureParameter; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + public function doFoo() + { + $a = function (string $test = null) { + assertType('string|null', $test); + return $test; + }; + assertType('string|null', $a()); + + $b = fn (string $test = null) => $test; + assertType('string|null', $b()); + + fn (string $test = null): string => assertType('string|null', $test); + } + +} diff --git a/tests/PHPStan/Analyser/data/number_format.php b/tests/PHPStan/Analyser/data/number_format.php index 271e501825..eb4d2a81ca 100644 --- a/tests/PHPStan/Analyser/data/number_format.php +++ b/tests/PHPStan/Analyser/data/number_format.php @@ -12,7 +12,7 @@ assertType('string', number_format(1002.7, 3, 'b', null)); assertType('string', number_format(1002.7, 3, 'b', '')); -assertType('string&numeric', number_format(1002.7, 3, '.', '')); -assertType('string&numeric', number_format(1002.7, 3, null, '')); -assertType('string&numeric', number_format(1002.7, 3, '', '')); +assertType('numeric-string', number_format(1002.7, 3, '.', '')); +assertType('numeric-string', number_format(1002.7, 3, null, '')); +assertType('numeric-string', number_format(1002.7, 3, '', '')); diff --git a/tests/PHPStan/Analyser/data/pdo-prepare.php b/tests/PHPStan/Analyser/data/pdo-prepare.php new file mode 100644 index 0000000000..72c4ab39c6 --- /dev/null +++ b/tests/PHPStan/Analyser/data/pdo-prepare.php @@ -0,0 +1,17 @@ +prepare('DELETE FROM log'); + assertType('(PDOStatement|false)', $logDeleteQuery); + } + +} diff --git a/tests/PHPStan/Analyser/data/phpdoc-pseudotype-global.php b/tests/PHPStan/Analyser/data/phpdoc-pseudotype-global.php index 75ed167f90..b8d6c138de 100644 --- a/tests/PHPStan/Analyser/data/phpdoc-pseudotype-global.php +++ b/tests/PHPStan/Analyser/data/phpdoc-pseudotype-global.php @@ -22,7 +22,7 @@ function () { $double = doFoo(); assertType('float|int', $number); - assertType('float|int|(string&numeric)', $numeric); + assertType('float|int|numeric-string', $numeric); assertType('bool', $boolean); assertType('resource', $resource); assertType('*NEVER*', $never); diff --git a/tests/PHPStan/Analyser/data/phpdoc-pseudotype-namespace.php b/tests/PHPStan/Analyser/data/phpdoc-pseudotype-namespace.php index b7ff3c7fda..c13d96db75 100644 --- a/tests/PHPStan/Analyser/data/phpdoc-pseudotype-namespace.php +++ b/tests/PHPStan/Analyser/data/phpdoc-pseudotype-namespace.php @@ -8,7 +8,6 @@ class Number {} class Numeric {} class Boolean {} class Resource {} -class Never {} class Double {} function () { @@ -21,9 +20,6 @@ function () { /** @var Numeric $numeric */ $numeric = doFoo(); - /** @var Never $never */ - $never = doFoo(); - /** @var Resource $resource */ $resource = doFoo(); @@ -34,6 +30,5 @@ function () { assertType('PhpdocPseudoTypesNamespace\Numeric', $numeric); assertType('PhpdocPseudoTypesNamespace\Boolean', $boolean); assertType('PhpdocPseudoTypesNamespace\Resource', $resource); - assertType('PhpdocPseudoTypesNamespace\Never', $never); assertType('PhpdocPseudoTypesNamespace\Double', $double); }; diff --git a/tests/PHPStan/Analyser/data/predefined-constants-32bit.php b/tests/PHPStan/Analyser/data/predefined-constants-32bit.php new file mode 100644 index 0000000000..8d0fe24eaf --- /dev/null +++ b/tests/PHPStan/Analyser/data/predefined-constants-32bit.php @@ -0,0 +1,7 @@ +', PHP_FLOAT_DIG); +assertType('float', PHP_FLOAT_EPSILON); +assertType('float', PHP_FLOAT_MIN); +assertType('float', PHP_FLOAT_MAX); +assertType('int<1, max>', PHP_FD_SETSIZE); diff --git a/tests/PHPStan/Analyser/data/predefined-constants-php74.php b/tests/PHPStan/Analyser/data/predefined-constants-php74.php new file mode 100644 index 0000000000..e0b7f0d156 --- /dev/null +++ b/tests/PHPStan/Analyser/data/predefined-constants-php74.php @@ -0,0 +1,7 @@ +', PHP_MAJOR_VERSION); +assertType('int<0, max>', PHP_MINOR_VERSION); +assertType('int<0, max>', PHP_RELEASE_VERSION); +assertType('int<50207, max>', PHP_VERSION_ID); +assertType('string', PHP_EXTRA_VERSION); +assertType('0|1', PHP_ZTS); +assertType('0|1', PHP_DEBUG); +assertType('int<1, max>', PHP_MAXPATHLEN); +assertType('non-empty-string', PHP_OS); +assertType('\'apache\'|\'apache2handler\'|\'cgi\'|\'cli\'|\'cli-server\'|\'embed\'|\'fpm-fcgi\'|\'litespeed\'|\'phpdbg\'|non-empty-string', PHP_SAPI); +assertType("'\n'|'\r\n'", PHP_EOL); +assertType('4|8', PHP_INT_SIZE); +assertType('string', DEFAULT_INCLUDE_PATH); +assertType('string', PEAR_INSTALL_DIR); +assertType('string', PEAR_EXTENSION_DIR); +assertType('non-empty-string', PHP_EXTENSION_DIR); +assertType('non-empty-string', PHP_PREFIX); +assertType('non-empty-string', PHP_BINDIR); +assertType('non-empty-string', PHP_BINARY); +assertType('non-empty-string', PHP_MANDIR); +assertType('non-empty-string', PHP_LIBDIR); +assertType('non-empty-string', PHP_DATADIR); +assertType('non-empty-string', PHP_SYSCONFDIR); +assertType('non-empty-string', PHP_LOCALSTATEDIR); +assertType('non-empty-string', PHP_CONFIG_FILE_PATH); +assertType('string', PHP_CONFIG_FILE_SCAN_DIR); +assertType('\'dll\'|\'so\'', PHP_SHLIB_SUFFIX); +assertType('1', E_ERROR); +assertType('2', E_WARNING); +assertType('4', E_PARSE); +assertType('8', E_NOTICE); +assertType('16', E_CORE_ERROR); +assertType('32', E_CORE_WARNING); +assertType('64', E_COMPILE_ERROR); +assertType('128', E_COMPILE_WARNING); +assertType('256', E_USER_ERROR); +assertType('512', E_USER_WARNING); +assertType('1024', E_USER_NOTICE); +assertType('4096', E_RECOVERABLE_ERROR); +assertType('8192', E_DEPRECATED); +assertType('16384', E_USER_DEPRECATED); +assertType('32767', E_ALL); +assertType('2048', E_STRICT); +assertType('int<0, max>', __COMPILER_HALT_OFFSET__); +assertType('true', true); +assertType('false', false); +assertType('null', null); + +// core other, https://www.php.net/manual/en/info.constants.php +assertType('int<4, max>', PHP_WINDOWS_VERSION_MAJOR); +assertType('int<0, max>', PHP_WINDOWS_VERSION_MINOR); +assertType('int<1, max>', PHP_WINDOWS_VERSION_BUILD); + +// dir, https://www.php.net/manual/en/dir.constants.php +assertType('\'/\'|\'\\\\\'', DIRECTORY_SEPARATOR); +assertType('\':\'|\';\'', PATH_SEPARATOR); + +// iconv, https://www.php.net/manual/en/iconv.constants.php +assertType('non-empty-string', ICONV_IMPL); + +// libxml, https://www.php.net/manual/en/libxml.constants.php +assertType('int<1, max>', LIBXML_VERSION); +assertType('non-empty-string', LIBXML_DOTTED_VERSION); + +// openssl, https://www.php.net/manual/en/openssl.constants.php +assertType('int<1, max>', OPENSSL_VERSION_NUMBER); + +// other +assertType('bool', ZEND_DEBUG_BUILD); +assertType('bool', ZEND_THREAD_SAFE); diff --git a/tests/PHPStan/Analyser/data/preg_filter.php b/tests/PHPStan/Analyser/data/preg_filter.php new file mode 100644 index 0000000000..8eb5826df0 --- /dev/null +++ b/tests/PHPStan/Analyser/data/preg_filter.php @@ -0,0 +1,44 @@ +', preg_filter($pattern, $replace, $subject)); + } + + function doFoo1() { + $subject = array('1', 'a', '2', 'b', '3', 'A', 'B', '4'); + assertType('array', preg_filter('/\d/', '$0', $subject)); + + $subject = 'hallo'; + assertType('string|null', preg_filter('/\d/', '$0', $subject)); + } + + function doFoo2() { + $subject = 123; + assertType('array|string|null', preg_filter('/\d/', '$0', $subject)); + + $subject = 123.123; + assertType('array|string|null', preg_filter('/\d/', '$0', $subject)); + } + + public function dooFoo3(string $pattern, string $replace) { + assertType('array|string|null', preg_filter($pattern, $replace)); + assertType('array|string|null', preg_filter($pattern)); + assertType('array|string|null', preg_filter()); + } + + function bug664() { + assertType('string|null', preg_filter(['#foo#'], ['bar'], 'subject')); + + assertType('array', preg_filter(['#foo#'], ['bar'], ['subject'])); + } +} diff --git a/tests/PHPStan/Analyser/data/preg_match_php7.php b/tests/PHPStan/Analyser/data/preg_match_php7.php new file mode 100644 index 0000000000..2e435cbf87 --- /dev/null +++ b/tests/PHPStan/Analyser/data/preg_match_php7.php @@ -0,0 +1,12 @@ +|false|null', preg_match_all('{}', '')); + } +} diff --git a/tests/PHPStan/Analyser/data/preg_match_php8.php b/tests/PHPStan/Analyser/data/preg_match_php8.php new file mode 100644 index 0000000000..a6f5c5b6ff --- /dev/null +++ b/tests/PHPStan/Analyser/data/preg_match_php8.php @@ -0,0 +1,13 @@ +|false', preg_match_all('{}', '')); + + } +} diff --git a/tests/PHPStan/Analyser/data/preg_split.php b/tests/PHPStan/Analyser/data/preg_split.php index 84a7223dc8..bb89a67175 100644 --- a/tests/PHPStan/Analyser/data/preg_split.php +++ b/tests/PHPStan/Analyser/data/preg_split.php @@ -2,7 +2,29 @@ use function PHPStan\Testing\assertType; -assertType('array|false', preg_split('/-/', '1-2-3')); -assertType('array|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY)); -assertType('array|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_OFFSET_CAPTURE)); -assertType('array|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); +class HelloWorld +{ + public function doFoo() + { + assertType('array|false', preg_split('/-/', '1-2-3')); + assertType('array|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY)); + assertType('array}>|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_OFFSET_CAPTURE)); + assertType('array}>|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE)); + } + + /** + * @param string $pattern + * @param string $subject + * @param int $limit + * @param int $flags PREG_SPLIT_NO_EMPTY or PREG_SPLIT_DELIM_CAPTURE + * @return list + * @phpstan-return list}> + */ + public static function splitWithOffset($pattern, $subject, $limit = -1, $flags = 0) + { + assertType('array}>|false', preg_split($pattern, $subject, $limit, $flags | PREG_SPLIT_OFFSET_CAPTURE)); + assertType('array}>|false', preg_split($pattern, $subject, $limit, PREG_SPLIT_OFFSET_CAPTURE | $flags)); + + assertType('array}>|false', preg_split($pattern, $subject, $limit, PREG_SPLIT_OFFSET_CAPTURE | $flags | PREG_SPLIT_NO_EMPTY)); + } +} diff --git a/tests/PHPStan/Analyser/data/proc_get_status.php b/tests/PHPStan/Analyser/data/proc_get_status.php index 449b4fd0ff..54f63e8094 100644 --- a/tests/PHPStan/Analyser/data/proc_get_status.php +++ b/tests/PHPStan/Analyser/data/proc_get_status.php @@ -11,5 +11,5 @@ function ($r): void { return; } - assertType('array(\'command\' => string, \'pid\' => int, \'running\' => bool, \'signaled\' => bool, \'stopped\' => bool, \'exitcode\' => int, \'termsig\' => int, \'stopsig\' => int)', $status); + assertType('array{command: string, pid: int, running: bool, signaled: bool, stopped: bool, exitcode: int, termsig: int, stopsig: int}', $status); }; diff --git a/tests/PHPStan/Analyser/data/property-assign-intersection-static-type-bug.php b/tests/PHPStan/Analyser/data/property-assign-intersection-static-type-bug.php index 7b66441340..ff67a805f3 100644 --- a/tests/PHPStan/Analyser/data/property-assign-intersection-static-type-bug.php +++ b/tests/PHPStan/Analyser/data/property-assign-intersection-static-type-bug.php @@ -13,6 +13,14 @@ public function __construct(string $foo) $this->foo = $foo; } + + /** + * @return string + */ + public function getFoo(): string + { + return $this->foo; + } } class Frontend extends Base diff --git a/tests/PHPStan/Analyser/data/property-template-tag.php b/tests/PHPStan/Analyser/data/property-template-tag.php new file mode 100644 index 0000000000..11310ed98d --- /dev/null +++ b/tests/PHPStan/Analyser/data/property-template-tag.php @@ -0,0 +1,21 @@ +, array>>> */ + private array $objectsByKey = array(); + + public function LoadObjectsByKey() : void + { + assertType('array, array>>>', $this->objectsByKey); + } +} diff --git a/tests/PHPStan/Analyser/data/random-int.php b/tests/PHPStan/Analyser/data/random-int.php index 709f9282f0..4e2df9ef57 100644 --- a/tests/PHPStan/Analyser/data/random-int.php +++ b/tests/PHPStan/Analyser/data/random-int.php @@ -22,9 +22,9 @@ function (int $i) { }; assertType('0', random_int(0, 0)); -assertType('int', random_int(PHP_INT_MIN, PHP_INT_MAX)); -assertType('int<0, max>', random_int(0, PHP_INT_MAX)); -assertType('int', random_int(PHP_INT_MIN, 0)); +assertType('int<-9223372036854775808, 9223372036854775807>', random_int(PHP_INT_MIN, PHP_INT_MAX)); +assertType('int<0, 9223372036854775807>', random_int(0, PHP_INT_MAX)); +assertType('int<-9223372036854775808, 0>', random_int(PHP_INT_MIN, 0)); assertType('int<-1, 1>', random_int(-1, 1)); assertType('int<0, 30>', random_int(0, random_int(0, 30))); assertType('int<0, 100>', random_int(random_int(0, 10), 100)); @@ -36,4 +36,7 @@ function (int $i) { assertType('int<0, 1>', random_int(random_int(0, 1), 1)); assertType('int<-5, 5>', random_int(random_int(-5, 0), random_int(0, 5))); -assertType('int', random_int(random_int(PHP_INT_MIN, 0), random_int(0, PHP_INT_MAX))); +assertType('int<-9223372036854775808, 9223372036854775807>', random_int(random_int(PHP_INT_MIN, 0), random_int(0, PHP_INT_MAX))); + +assertType('int<-5, 5>', rand(-5, 5)); +assertType('int<0, max>', rand()); diff --git a/tests/PHPStan/Analyser/data/range-numeric-string.php b/tests/PHPStan/Analyser/data/range-numeric-string.php new file mode 100644 index 0000000000..faddec206b --- /dev/null +++ b/tests/PHPStan/Analyser/data/range-numeric-string.php @@ -0,0 +1,22 @@ +', range($a, $b)); + } + +} diff --git a/tests/PHPStan/Analyser/data/reflectionclass-issue-5511-php8.php b/tests/PHPStan/Analyser/data/reflectionclass-issue-5511-php8.php new file mode 100644 index 0000000000..b657010861 --- /dev/null +++ b/tests/PHPStan/Analyser/data/reflectionclass-issue-5511-php8.php @@ -0,0 +1,79 @@ + $genericClassName + */ +function testGetAttributes( + \ReflectionClass $reflectionClass, + \ReflectionMethod $reflectionMethod, + \ReflectionParameter $reflectionParameter, + \ReflectionProperty $reflectionProperty, + \ReflectionClassConstant $reflectionClassConstant, + \ReflectionFunction $reflectionFunction, + string $str, + string $className, + string $genericClassName +): void +{ + $classAll = $reflectionClass->getAttributes(); + $classAbc1 = $reflectionClass->getAttributes(Abc::class); + $classAbc2 = $reflectionClass->getAttributes(Abc::class, \ReflectionAttribute::IS_INSTANCEOF); + $classGCN = $reflectionClass->getAttributes($genericClassName); + $classCN = $reflectionClass->getAttributes($className); + $classStr = $reflectionClass->getAttributes($str); + $classNonsense = $reflectionClass->getAttributes("some random string"); + + assertType('array>', $classAll); + assertType('array>', $classAbc1); + assertType('array>', $classAbc2); + assertType('array>', $classGCN); + assertType('array>', $classCN); + assertType('array>', $classStr); + assertType('array>', $classNonsense); + + $methodAll = $reflectionMethod->getAttributes(); + $methodAbc = $reflectionMethod->getAttributes(Abc::class); + assertType('array>', $methodAll); + assertType('array>', $methodAbc); + + $paramAll = $reflectionParameter->getAttributes(); + $paramAbc = $reflectionParameter->getAttributes(Abc::class); + assertType('array>', $paramAll); + assertType('array>', $paramAbc); + + $propAll = $reflectionProperty->getAttributes(); + $propAbc = $reflectionProperty->getAttributes(Abc::class); + assertType('array>', $propAll); + assertType('array>', $propAbc); + + $constAll = $reflectionClassConstant->getAttributes(); + $constAbc = $reflectionClassConstant->getAttributes(Abc::class); + assertType('array>', $constAll); + assertType('array>', $constAbc); + + $funcAll = $reflectionFunction->getAttributes(); + $funcAbc = $reflectionFunction->getAttributes(Abc::class); + assertType('array>', $funcAll); + assertType('array>', $funcAbc); +} + +/** + * @param \ReflectionAttribute $ra + */ +function testNewInstance(\ReflectionAttribute $ra): void +{ + assertType('ReflectionAttribute', $ra); + $abc = $ra->newInstance(); + assertType(Abc::class, $abc); +} diff --git a/tests/PHPStan/Analyser/data/root-scope-maybe-defined.php b/tests/PHPStan/Analyser/data/root-scope-maybe-defined.php index b8fa432f9b..800858e827 100644 --- a/tests/PHPStan/Analyser/data/root-scope-maybe-defined.php +++ b/tests/PHPStan/Analyser/data/root-scope-maybe-defined.php @@ -16,7 +16,7 @@ \PHPStan\Testing\assertType('1', $baz); } -\PHPStan\Testing\assertType('mixed', $baz); +\PHPStan\Testing\assertType('mixed~null', $baz); function () { \PHPStan\Testing\assertVariableCertainty(TrinaryLogic::createNo(), $foo); diff --git a/tests/PHPStan/Analyser/data/round-php8.php b/tests/PHPStan/Analyser/data/round-php8.php new file mode 100644 index 0000000000..85428a2fd6 --- /dev/null +++ b/tests/PHPStan/Analyser/data/round-php8.php @@ -0,0 +1,61 @@ +|int<32, max>|string)', $x); + assertType('(int|int<32, max>|string)', $y); + + assertType('bool', $x < $y); + assertType('bool', $x <= $y); + assertType('bool', $x > $y); + assertType('bool', $x >= $y); + + return $x < $y ? 1 : -1; + }); + } + +} diff --git a/tests/PHPStan/Analyser/data/splfixedarray-iterator-types.php b/tests/PHPStan/Analyser/data/splfixedarray-iterator-types.php new file mode 100644 index 0000000000..4c512bea06 --- /dev/null +++ b/tests/PHPStan/Analyser/data/splfixedarray-iterator-types.php @@ -0,0 +1,17 @@ + + */ + public $array; + + public function dump() : void{ + foreach($this->array as $id => $v){ + \PHPStan\Testing\assertType('int|null', $this->array[$id]); + \PHPStan\Testing\assertType('int|null', $v); + } + } +} diff --git a/tests/PHPStan/Analyser/data/sscanf.php b/tests/PHPStan/Analyser/data/sscanf.php new file mode 100644 index 0000000000..2b7105f939 --- /dev/null +++ b/tests/PHPStan/Analyser/data/sscanf.php @@ -0,0 +1,6 @@ +', $strtotimeNow); + +$strtotimeInvalid = strtotime('4 qm'); +assertType('false', $strtotimeInvalid); + +$strtotimeUnknown = strtotime(rand(0, 1) === 0 ? 'now': '4 qm'); +assertType('int|false', $strtotimeUnknown); + +$strtotimeUnknown2 = strtotime($undefined); +assertType('(int|false)', $strtotimeUnknown2); + +$strtotimeCrash = strtotime(); +assertType('int|false', $strtotimeCrash); + +$strtotimeWithBase = strtotime('+2 days', time()); +assertType('int', $strtotimeWithBase); + +$strtotimePositiveInt = strtotime('1990-01-01 12:00:00 UTC'); +assertType('int<1, max>', $strtotimePositiveInt); + +$strtotimeNegativeInt = strtotime('1969-12-31 12:00:00 UTC'); +assertType('int', $strtotimeNegativeInt); diff --git a/tests/PHPStan/Analyser/data/strval.php b/tests/PHPStan/Analyser/data/strval.php index 04689cbcb8..f6fa62fe93 100644 --- a/tests/PHPStan/Analyser/data/strval.php +++ b/tests/PHPStan/Analyser/data/strval.php @@ -1,19 +1,93 @@ $class */ -function test(string $class) +function strvalTest(string $string, string $class): void { + assertType('null', strval()); assertType('\'foo\'', strval('foo')); + assertType('string', strval($string)); assertType('\'\'', strval(null)); + assertType('\'\'', strval(false)); + assertType('\'1\'', strval(true)); assertType('\'\'|\'1\'', strval(rand(0, 1) === 0)); - assertType('string&numeric', strval(rand())); - assertType('string&numeric', strval(rand() * 0.1)); - assertType('string&numeric', strval(strval(rand()))); + assertType('\'42\'', strval(42)); + assertType('numeric-string', strval(rand())); + assertType('numeric-string', strval(rand() * 0.1)); + assertType('numeric-string', strval(strval(rand()))); assertType('class-string', strval($class)); + assertType('string', strval(new \Exception())); + assertType('*ERROR*', strval(new \stdClass())); +} + +function intvalTest(string $string): void +{ + assertType('null', intval()); + assertType('42', intval('42')); + assertType('0', intval('foo')); + assertType('int', intval($string)); + assertType('0', intval(null)); + assertType('0', intval(false)); + assertType('1', intval(true)); + assertType('0|1', intval(rand(0, 1) === 0)); + assertType('42', intval(42)); + assertType('int<0, max>', intval(rand())); + assertType('int', intval(rand() * 0.1)); + assertType('0', intval([])); + assertType('1', intval([null])); +} + +function floatvalTest(string $string): void +{ + assertType('null', floatval()); + assertType('3.14', floatval('3.14')); + assertType('0.0', floatval('foo')); + assertType('float', floatval($string)); + assertType('0.0', floatval(null)); + assertType('0.0', floatval(false)); + assertType('1.0', floatval(true)); + assertType('0.0|1.0', floatval(rand(0, 1) === 0)); + assertType('42.0', floatval(42)); + assertType('float', floatval(rand())); + assertType('float', floatval(rand() * 0.1)); + assertType('0.0', floatval([])); + assertType('1.0', floatval([null])); +} + +function boolvalTest(string $string): void +{ + assertType('null', boolval()); + assertType('false', boolval('')); + assertType('true', boolval('foo')); + assertType('bool', boolval($string)); + assertType('false', boolval(null)); + assertType('false', boolval(false)); + assertType('true', boolval(true)); + assertType('bool', boolval(rand(0, 1) === 0)); + assertType('true', boolval(42)); + assertType('bool', boolval(rand())); + assertType('bool', boolval(rand() * 0.1)); + assertType('false', boolval([])); + assertType('true', boolval([null])); + assertType('true', boolval(new \stdClass())); +} + +function arrayTest(array $a): void +{ + assertType('0|1', intval($a)); + assertType('0.0|1.0', floatval($a)); + assertType('bool', boolval($a)); +} + +/** @param non-empty-array $a */ +function nonEmptyArrayTest(array $a): void +{ + assertType('1', intval($a)); + assertType('1.0', floatval($a)); + assertType('true', boolval($a)); } diff --git a/tests/PHPStan/Analyser/data/template-null-bound.php b/tests/PHPStan/Analyser/data/template-null-bound.php new file mode 100644 index 0000000000..66f1346914 --- /dev/null +++ b/tests/PHPStan/Analyser/data/template-null-bound.php @@ -0,0 +1,26 @@ +doFoo(null)); + assertType('int', $f->doFoo(1)); + assertType('int|null', $f->doFoo($i)); +}; diff --git a/tests/PHPStan/Analyser/data/two-same-classes.php b/tests/PHPStan/Analyser/data/two-same-classes.php index a5cf574043..872edbed82 100644 --- a/tests/PHPStan/Analyser/data/two-same-classes.php +++ b/tests/PHPStan/Analyser/data/two-same-classes.php @@ -13,6 +13,14 @@ public function doFoo(): void echo self::FOO_CONST; } + /** + * @return string + */ + public function getProp() + { + return $this->prop; + } + } if (rand(0, 0)) { @@ -32,5 +40,37 @@ public function doFoo(): void echo self::FOO_CONST; } + /** + * @return int + */ + public function getProp() + { + return $this->prop; + } + + /** + * @param int $prop + */ + public function setProp($prop): void + { + $this->prop = $prop; + } + + /** + * @return int + */ + public function getProp2() + { + return $this->prop2; + } + + /** + * @param int $prop2 + */ + public function setProp2($prop2): void + { + $this->prop2 = $prop2; + } + } } diff --git a/tests/PHPStan/Analyser/data/type-aliases.php b/tests/PHPStan/Analyser/data/type-aliases.php index 9ad1e39918..a6342387ab 100644 --- a/tests/PHPStan/Analyser/data/type-aliases.php +++ b/tests/PHPStan/Analyser/data/type-aliases.php @@ -181,7 +181,7 @@ class UsesTrait1 /** @param Test $a */ public function doBar($a) { - assertType('array(string, int)', $a); + assertType('array{string, int}', $a); assertType(Test::class, $this->doFoo()); } diff --git a/tests/PHPStan/Analyser/data/type-specifying-extensions-1-false.php b/tests/PHPStan/Analyser/data/type-specifying-extensions-1-false.php new file mode 100644 index 0000000000..2e4d900db3 --- /dev/null +++ b/tests/PHPStan/Analyser/data/type-specifying-extensions-1-false.php @@ -0,0 +1,14 @@ +assertString($foo); +$test = \PHPStan\Tests\AssertionClass::assertInt($bar); + +assertType('string|null', $foo); +assertType('int|null', $bar); diff --git a/tests/PHPStan/Analyser/data/type-specifying-extensions.php b/tests/PHPStan/Analyser/data/type-specifying-extensions-1-null.php similarity index 68% rename from tests/PHPStan/Analyser/data/type-specifying-extensions.php rename to tests/PHPStan/Analyser/data/type-specifying-extensions-1-null.php index 512fd10f87..5a9326d3f4 100644 --- a/tests/PHPStan/Analyser/data/type-specifying-extensions.php +++ b/tests/PHPStan/Analyser/data/type-specifying-extensions-1-null.php @@ -1,5 +1,7 @@ assertString($foo); $test = \PHPStan\Tests\AssertionClass::assertInt($bar); -die; +assertType('string', $foo); +assertType('int', $bar); diff --git a/tests/PHPStan/Analyser/data/type-specifying-extensions-1-true.php b/tests/PHPStan/Analyser/data/type-specifying-extensions-1-true.php new file mode 100644 index 0000000000..5a9326d3f4 --- /dev/null +++ b/tests/PHPStan/Analyser/data/type-specifying-extensions-1-true.php @@ -0,0 +1,14 @@ +assertString($foo); +$test = \PHPStan\Tests\AssertionClass::assertInt($bar); + +assertType('string', $foo); +assertType('int', $bar); diff --git a/tests/PHPStan/Analyser/data/type-specifying-extensions-2-false.php b/tests/PHPStan/Analyser/data/type-specifying-extensions-2-false.php new file mode 100644 index 0000000000..8016cee235 --- /dev/null +++ b/tests/PHPStan/Analyser/data/type-specifying-extensions-2-false.php @@ -0,0 +1,14 @@ +assertString($foo) && \PHPStan\Tests\AssertionClass::assertInt($bar)) { +} + +assertType('string|null', $foo); +assertType('int|null', $bar); diff --git a/tests/PHPStan/Analyser/data/type-specifying-extensions-2-null.php b/tests/PHPStan/Analyser/data/type-specifying-extensions-2-null.php new file mode 100644 index 0000000000..8016cee235 --- /dev/null +++ b/tests/PHPStan/Analyser/data/type-specifying-extensions-2-null.php @@ -0,0 +1,14 @@ +assertString($foo) && \PHPStan\Tests\AssertionClass::assertInt($bar)) { +} + +assertType('string|null', $foo); +assertType('int|null', $bar); diff --git a/tests/PHPStan/Analyser/data/type-specifying-extensions-2-true.php b/tests/PHPStan/Analyser/data/type-specifying-extensions-2-true.php new file mode 100644 index 0000000000..8016cee235 --- /dev/null +++ b/tests/PHPStan/Analyser/data/type-specifying-extensions-2-true.php @@ -0,0 +1,14 @@ +assertString($foo) && \PHPStan\Tests\AssertionClass::assertInt($bar)) { +} + +assertType('string|null', $foo); +assertType('int|null', $bar); diff --git a/tests/PHPStan/Analyser/data/type-specifying-extensions2.php b/tests/PHPStan/Analyser/data/type-specifying-extensions-3-false.php similarity index 68% rename from tests/PHPStan/Analyser/data/type-specifying-extensions2.php rename to tests/PHPStan/Analyser/data/type-specifying-extensions-3-false.php index bda2e87e55..af253cc5f8 100644 --- a/tests/PHPStan/Analyser/data/type-specifying-extensions2.php +++ b/tests/PHPStan/Analyser/data/type-specifying-extensions-3-false.php @@ -1,11 +1,13 @@ assertString($foo) && \PHPStan\Tests\AssertionClass::assertInt($bar)) { + assertType('string', $foo); + assertType('int', $bar); } - -die; diff --git a/tests/PHPStan/Analyser/data/type-specifying-extensions3.php b/tests/PHPStan/Analyser/data/type-specifying-extensions-3-null.php similarity index 67% rename from tests/PHPStan/Analyser/data/type-specifying-extensions3.php rename to tests/PHPStan/Analyser/data/type-specifying-extensions-3-null.php index 37a1033c2d..af253cc5f8 100644 --- a/tests/PHPStan/Analyser/data/type-specifying-extensions3.php +++ b/tests/PHPStan/Analyser/data/type-specifying-extensions-3-null.php @@ -1,10 +1,13 @@ assertString($foo) && \PHPStan\Tests\AssertionClass::assertInt($bar)) { - die; + assertType('string', $foo); + assertType('int', $bar); } diff --git a/tests/PHPStan/Analyser/data/type-specifying-extensions-3-true.php b/tests/PHPStan/Analyser/data/type-specifying-extensions-3-true.php new file mode 100644 index 0000000000..92e26fdccd --- /dev/null +++ b/tests/PHPStan/Analyser/data/type-specifying-extensions-3-true.php @@ -0,0 +1,13 @@ +assertString($foo) && \PHPStan\Tests\AssertionClass::assertInt($bar)) { + assertType('string|null', $foo); + assertType('int|null', $bar); +} diff --git a/tests/PHPStan/Analyser/data/union-intersection.php b/tests/PHPStan/Analyser/data/union-intersection.php index 0e0edc880a..03266cd2c8 100644 --- a/tests/PHPStan/Analyser/data/union-intersection.php +++ b/tests/PHPStan/Analyser/data/union-intersection.php @@ -5,6 +5,7 @@ class WithFoo { + /** @var 1 */ const FOO_CONSTANT = 1; /** @var Foo */ @@ -25,7 +26,10 @@ public static function doStaticFoo(): Foo class WithFooAndBar { + /** @var 1 */ const FOO_CONSTANT = 1; + + /** @var 1 */ const BAR_CONSTANT = 1; /** @var AnotherFoo */ @@ -59,7 +63,10 @@ public static function doStaticBar(): Bar interface WithFooAndBarInterface { + /** @var 1 */ const FOO_CONSTANT = 1; + + /** @var 1 */ const BAR_CONSTANT = 1; public function doFoo(): AnotherFoo; @@ -80,6 +87,7 @@ interface SomeInterface class Dolor { + /** @var array{1, 2, 3} */ const PARENT_CONSTANT = [1, 2, 3]; } diff --git a/tests/PHPStan/Analyser/data/value-of.php b/tests/PHPStan/Analyser/data/value-of.php new file mode 100644 index 0000000000..c05fda0fda --- /dev/null +++ b/tests/PHPStan/Analyser/data/value-of.php @@ -0,0 +1,38 @@ + $code + */ + public static function foo(string $code): void + { + assertType('\'jfk\'|\'lga\'', $code); + } + + /** + * @param value-of<'jfk'> $code + */ + public static function bar(string $code): void + { + assertType('string', $code); + } + + /** + * @param value-of<'jfk'|'lga'> $code + */ + public static function baz(string $code): void + { + assertType('string', $code); + } +} diff --git a/tests/PHPStan/Analyser/data/variadic-parameter-php8.php b/tests/PHPStan/Analyser/data/variadic-parameter-php8.php new file mode 100644 index 0000000000..52de2402f1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/variadic-parameter-php8.php @@ -0,0 +1,18 @@ +', $args); + assertType('mixed', $args['foo']); + assertType('mixed', $args['bar']); +} + +function bar(string ...$args) +{ + assertType('array', $args); +} + diff --git a/tests/PHPStan/Analyser/data/weird-array_key_exists-issue.php b/tests/PHPStan/Analyser/data/weird-array_key_exists-issue.php new file mode 100644 index 0000000000..95d28f327c --- /dev/null +++ b/tests/PHPStan/Analyser/data/weird-array_key_exists-issue.php @@ -0,0 +1,51 @@ +> + */ + public function doFoo(array $data): array + { + if (count($data) === 0) { + return []; + } + + arsort($data); + $locationData = []; + $otherData = []; + $i = 0; + $total = array_sum($data); + foreach ($data as $location => $count) { + assertType('int<0, max>', $i); + if ($i < 5) { + $locationData[$location] = [ + 'abs' => $count, + 'rel' => $count / $total * 100, + ]; + } else { + $key = 'Ostatní'; + assertType('bool', array_key_exists($key, $otherData)); + assertType('array<\'Ostatní\', array{abs: int, rel: (float|int)}>', $otherData); + if (!array_key_exists($key, $otherData)) { + $otherData[$key] = [ + 'abs' => 0, + 'rel' => 0, + ]; + assertType('non-empty-array<\'Ostatní\', array{abs: int, rel: (float|int)}>', $otherData); + } + $otherData[$key]['abs'] += $count; + $otherData[$key]['rel'] += $count / $total * 100; + assertType('non-empty-array<\'Ostatní\', array{abs: int, rel: (float|int)}>', $otherData); + } + $i++; + } + + return array_merge($locationData, $otherData); + } +} diff --git a/tests/PHPStan/Analyser/dynamic-return-type.neon b/tests/PHPStan/Analyser/dynamic-return-type.neon new file mode 100644 index 0000000000..b8bd815fb4 --- /dev/null +++ b/tests/PHPStan/Analyser/dynamic-return-type.neon @@ -0,0 +1,29 @@ +services: + - + class: PHPStan\Tests\GetByPrimaryDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Tests\OffsetGetDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Tests\CreateManagerForEntityDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicStaticMethodReturnTypeExtension + - + class: PHPStan\Tests\ConstructDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicStaticMethodReturnTypeExtension + - + class: PHPStan\Tests\ConstructWithoutConstructor + tags: + - phpstan.broker.dynamicStaticMethodReturnTypeExtension + - + class: PHPStan\Tests\GetSelfDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Tests\FooGetSelf + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension diff --git a/tests/PHPStan/Analyser/traitsCachingIssue/TraitsCachingIssueIntegrationTest.php b/tests/PHPStan/Analyser/traitsCachingIssue/TraitsCachingIssueIntegrationTest.php index 7b0fa2c07d..ee7dd1f215 100644 --- a/tests/PHPStan/Analyser/traitsCachingIssue/TraitsCachingIssueIntegrationTest.php +++ b/tests/PHPStan/Analyser/traitsCachingIssue/TraitsCachingIssueIntegrationTest.php @@ -4,7 +4,7 @@ use PHPStan\File\FileHelper; use PHPStan\File\FileReader; -use PHPStan\Testing\TestCase; +use PHPStan\Testing\PHPStanTestCase; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use function file_exists; @@ -12,7 +12,7 @@ /** * @group exec */ -class TraitsCachingIssueIntegrationTest extends TestCase +class TraitsCachingIssueIntegrationTest extends PHPStanTestCase { /** @var string|null */ diff --git a/tests/PHPStan/Analyser/traitsCachingIssue/phpstan.neon b/tests/PHPStan/Analyser/traitsCachingIssue/phpstan.neon index 799d286c7a..d20f1be41c 100644 --- a/tests/PHPStan/Analyser/traitsCachingIssue/phpstan.neon +++ b/tests/PHPStan/Analyser/traitsCachingIssue/phpstan.neon @@ -1,4 +1,4 @@ parameters: tmpDir: tmp - autoload_directories: + scanDirectories: - data diff --git a/tests/PHPStan/Broker/BrokerTest.php b/tests/PHPStan/Broker/BrokerTest.php index 6e32c59a4d..413cc7ec82 100644 --- a/tests/PHPStan/Broker/BrokerTest.php +++ b/tests/PHPStan/Broker/BrokerTest.php @@ -7,11 +7,10 @@ use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber; use PHPStan\Cache\Cache; use PHPStan\DependencyInjection\Reflection\DirectClassReflectionExtensionRegistryProvider; -use PHPStan\DependencyInjection\Type\DirectDynamicReturnTypeExtensionRegistryProvider; -use PHPStan\DependencyInjection\Type\DirectOperatorTypeSpecifyingExtensionRegistryProvider; use PHPStan\File\FileHelper; use PHPStan\File\SimpleRelativePathHelper; use PHPStan\Php\PhpVersion; +use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\PhpDoc\PhpDocNodeResolver; use PHPStan\PhpDoc\PhpDocStringResolver; use PHPStan\PhpDoc\StubPhpDocProvider; @@ -19,13 +18,14 @@ use PHPStan\Reflection\ReflectionProvider\SetterReflectionProviderProvider; use PHPStan\Reflection\Runtime\RuntimeReflectionProvider; use PHPStan\Reflection\SignatureMap\NativeFunctionReflectionProvider; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\FileTypeMapper; +use function spl_autoload_register; -class BrokerTest extends \PHPStan\Testing\TestCase +class BrokerTest extends PHPStanTestCase { - /** @var \PHPStan\Broker\Broker */ - private $broker; + private Broker $broker; protected function setUp(): void { @@ -38,42 +38,37 @@ protected function setUp(): void $anonymousClassNameHelper = new AnonymousClassNameHelper($fileHelper, $relativePathHelper); $classReflectionExtensionRegistryProvider = new DirectClassReflectionExtensionRegistryProvider([], []); - $dynamicReturnTypeExtensionRegistryProvider = new DirectDynamicReturnTypeExtensionRegistryProvider([], [], []); - $operatorTypeSpecifyingExtensionRegistryProvider = new DirectOperatorTypeSpecifyingExtensionRegistryProvider([]); $setterReflectionProviderProvider = new SetterReflectionProviderProvider(); $reflectionProvider = new RuntimeReflectionProvider( $setterReflectionProviderProvider, $classReflectionExtensionRegistryProvider, $this->createMock(FunctionReflectionFactory::class), - new FileTypeMapper($setterReflectionProviderProvider, $this->getParser(), $phpDocStringResolver, $phpDocNodeResolver, $this->createMock(Cache::class), $anonymousClassNameHelper), + new FileTypeMapper($setterReflectionProviderProvider, $this->getParser(), $phpDocStringResolver, $phpDocNodeResolver, $this->createMock(Cache::class), $anonymousClassNameHelper, self::getContainer()->getByType(PhpVersion::class), self::getContainer()->getByType(FileHelper::class)), + self::getContainer()->getByType(PhpDocInheritanceResolver::class), self::getContainer()->getByType(PhpVersion::class), self::getContainer()->getByType(NativeFunctionReflectionProvider::class), self::getContainer()->getByType(StubPhpDocProvider::class), - self::getContainer()->getByType(PhpStormStubsSourceStubber::class) + self::getContainer()->getByType(PhpStormStubsSourceStubber::class), ); $setterReflectionProviderProvider->setReflectionProvider($reflectionProvider); $this->broker = new Broker( $reflectionProvider, - $dynamicReturnTypeExtensionRegistryProvider, - $operatorTypeSpecifyingExtensionRegistryProvider, - [] + [], ); $classReflectionExtensionRegistryProvider->setBroker($this->broker); - $dynamicReturnTypeExtensionRegistryProvider->setBroker($this->broker); - $operatorTypeSpecifyingExtensionRegistryProvider->setBroker($this->broker); } public function testClassNotFound(): void { - $this->expectException(\PHPStan\Broker\ClassNotFoundException::class); + $this->expectException(ClassNotFoundException::class); $this->expectExceptionMessage('NonexistentClass'); $this->broker->getClass('NonexistentClass'); } public function testFunctionNotFound(): void { - $this->expectException(\PHPStan\Broker\FunctionNotFoundException::class); + $this->expectException(FunctionNotFoundException::class); $this->expectExceptionMessage('Function nonexistentFunction not found while trying to analyse it - discovering symbols is probably not configured properly.'); $scope = $this->createMock(Scope::class); @@ -84,7 +79,7 @@ public function testFunctionNotFound(): void public function testClassAutoloadingException(): void { - $this->expectException(\PHPStan\Broker\ClassAutoloadingException::class); + $this->expectException(ClassAutoloadingException::class); $this->expectExceptionMessage('thrown while looking for class NonexistentClass.'); spl_autoload_register(static function (): void { require_once __DIR__ . '/../Analyser/data/parse-error.php'; diff --git a/tests/PHPStan/Command/AnalyseApplicationIntegrationTest.php b/tests/PHPStan/Command/AnalyseApplicationIntegrationTest.php index 844dc62b09..277f63d451 100644 --- a/tests/PHPStan/Command/AnalyseApplicationIntegrationTest.php +++ b/tests/PHPStan/Command/AnalyseApplicationIntegrationTest.php @@ -7,11 +7,20 @@ use PHPStan\Command\Symfony\SymfonyOutput; use PHPStan\File\FuzzyRelativePathHelper; use PHPStan\File\NullRelativePathHelper; +use PHPStan\ShouldNotHappenException; +use PHPStan\Testing\PHPStanTestCase; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\StreamOutput; use Symfony\Component\Console\Style\SymfonyStyle; +use function file_exists; +use function fopen; +use function rewind; +use function sprintf; +use function stream_get_contents; +use function unlink; +use const DIRECTORY_SEPARATOR; -class AnalyseApplicationIntegrationTest extends \PHPStan\Testing\TestCase +class AnalyseApplicationIntegrationTest extends PHPStanTestCase { public function testExecuteOnAFile(): void @@ -26,7 +35,7 @@ public function testExecuteOnANonExistentPath(): void $output = $this->runPath($path, 1); $this->assertStringContainsString(sprintf( 'File %s does not exist.', - $path + $path, ), $output); } @@ -39,36 +48,33 @@ public function testExecuteOnAFileWithErrors(): void private function runPath(string $path, int $expectedStatusCode): string { - if (PHP_VERSION_ID >= 80000 && DIRECTORY_SEPARATOR === '\\') { - $this->markTestSkipped('Skipped because of https://github.com/symfony/symfony/issues/37508'); - } self::getContainer()->getByType(ResultCacheClearer::class)->clear(); $analyserApplication = self::getContainer()->getByType(AnalyseApplication::class); $resource = fopen('php://memory', 'w', false); if ($resource === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $output = new StreamOutput($resource); $symfonyOutput = new SymfonyOutput( $output, - new \PHPStan\Command\Symfony\SymfonyStyle(new SymfonyStyle($this->createMock(InputInterface::class), $output)) + new \PHPStan\Command\Symfony\SymfonyStyle(new SymfonyStyle($this->createMock(InputInterface::class), $output)), ); $memoryLimitFile = self::getContainer()->getParameter('memoryLimitFile'); $relativePathHelper = new FuzzyRelativePathHelper(new NullRelativePathHelper(), __DIR__, [], DIRECTORY_SEPARATOR); - $errorFormatter = new TableErrorFormatter($relativePathHelper, false); + $errorFormatter = new TableErrorFormatter($relativePathHelper, false, null); $analysisResult = $analyserApplication->analyse( [$path], true, $symfonyOutput, $symfonyOutput, false, - false, + true, null, null, - $this->createMock(InputInterface::class) + $this->createMock(InputInterface::class), ); if (file_exists($memoryLimitFile)) { unlink($memoryLimitFile); @@ -80,7 +86,7 @@ private function runPath(string $path, int $expectedStatusCode): string $contents = stream_get_contents($output->getStream()); if ($contents === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } return $contents; diff --git a/tests/PHPStan/Command/AnalyseCommandTest.php b/tests/PHPStan/Command/AnalyseCommandTest.php index 6754544607..68abcd2875 100644 --- a/tests/PHPStan/Command/AnalyseCommandTest.php +++ b/tests/PHPStan/Command/AnalyseCommandTest.php @@ -2,32 +2,38 @@ namespace PHPStan\Command; +use PHPStan\ShouldNotHappenException; +use PHPStan\Testing\PHPStanTestCase; use Symfony\Component\Console\Tester\CommandTester; +use Throwable; +use function chdir; +use function getcwd; +use function realpath; +use function sprintf; use const DIRECTORY_SEPARATOR; +use const PHP_EOL; /** * @group exec */ -class AnalyseCommandTest extends \PHPStan\Testing\TestCase +class AnalyseCommandTest extends PHPStanTestCase { /** - * @param string $dir - * @param string $file * @dataProvider autoDiscoveryPathsProvider */ public function testConfigurationAutoDiscovery(string $dir, string $file): void { $originalDir = getcwd(); if ($originalDir === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } chdir($dir); try { $output = $this->runCommand(1); $this->assertStringContainsString('Note: Using configuration file ' . $file . '.', $output); - } catch (\Throwable $e) { + } catch (Throwable $e) { chdir($originalDir); throw $e; } @@ -66,23 +72,26 @@ public static function autoDiscoveryPathsProvider(): array __DIR__ . '/test-autodiscover-dist', __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-dist' . DIRECTORY_SEPARATOR . 'phpstan.neon.dist', ], + [ + __DIR__ . '/test-autodiscover-dist-dot-neon', + __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-dist-dot-neon' . DIRECTORY_SEPARATOR . 'phpstan.dist.neon', + ], [ __DIR__ . '/test-autodiscover-priority', __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-priority' . DIRECTORY_SEPARATOR . 'phpstan.neon', ], + [ + __DIR__ . '/test-autodiscover-priority-dist-dot-neon', + __DIR__ . DIRECTORY_SEPARATOR . 'test-autodiscover-priority-dist-dot-neon' . DIRECTORY_SEPARATOR . 'phpstan.neon', + ], ]; } /** - * @param int $expectedStatusCode * @param array $parameters - * @return string */ private function runCommand(int $expectedStatusCode, array $parameters = []): string { - if (PHP_VERSION_ID >= 80000 && DIRECTORY_SEPARATOR === '\\') { - $this->markTestSkipped('Skipped because of https://github.com/symfony/symfony/issues/37508'); - } $commandTester = new CommandTester(new AnalyseCommand([])); $commandTester->execute([ diff --git a/tests/PHPStan/Command/AnalysisResultTest.php b/tests/PHPStan/Command/AnalysisResultTest.php index d72143624f..7ce1fdecd7 100644 --- a/tests/PHPStan/Command/AnalysisResultTest.php +++ b/tests/PHPStan/Command/AnalysisResultTest.php @@ -3,9 +3,9 @@ namespace PHPStan\Command; use PHPStan\Analyser\Error; -use PHPStan\Testing\TestCase; +use PHPStan\Testing\PHPStanTestCase; -final class AnalysisResultTest extends TestCase +final class AnalysisResultTest extends PHPStanTestCase { public function testErrorsAreSortedByFileNameAndLine(): void @@ -41,8 +41,8 @@ public function testErrorsAreSortedByFileNameAndLine(): void [], false, null, - true - ))->getFileSpecificErrors() + true, + ))->getFileSpecificErrors(), ); } diff --git a/tests/PHPStan/Command/CommandHelperTest.php b/tests/PHPStan/Command/CommandHelperTest.php index ff2a6fff2e..8c29068a98 100644 --- a/tests/PHPStan/Command/CommandHelperTest.php +++ b/tests/PHPStan/Command/CommandHelperTest.php @@ -2,11 +2,16 @@ namespace PHPStan\Command; +use PHPStan\ShouldNotHappenException; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Output\StreamOutput; +use function fopen; use function realpath; +use function rewind; +use function stream_get_contents; +use const DIRECTORY_SEPARATOR; /** * @group exec @@ -90,12 +95,7 @@ public function dataBegin(): array /** * @dataProvider dataBegin - * @param string $input - * @param string $expectedOutput - * @param string|null $projectConfigFile - * @param string|null $level * @param mixed[] $expectedParameters - * @param bool $expectException */ public function testBegin( string $input, @@ -103,12 +103,12 @@ public function testBegin( ?string $projectConfigFile, ?string $level, array $expectedParameters, - bool $expectException + bool $expectException, ): void { $resource = fopen('php://memory', 'w', false); if ($resource === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $output = new StreamOutput($resource); @@ -119,23 +119,21 @@ public function testBegin( [__DIR__], null, null, - null, [], $projectConfigFile, null, $level, false, - true ); if ($expectException) { $this->fail(); } - } catch (\PHPStan\Command\InceptionNotSuccessfulException $e) { + } catch (InceptionNotSuccessfulException) { if (!$expectException) { rewind($output->getStream()); $contents = stream_get_contents($output->getStream()); if ($contents === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $this->fail($contents); } @@ -145,7 +143,7 @@ public function testBegin( $contents = stream_get_contents($output->getStream()); if ($contents === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $this->assertStringContainsString($expectedOutput, $contents); @@ -166,22 +164,13 @@ public function dataParameters(): array [ __DIR__ . '/relative-paths/root.neon', [ - 'bootstrap' => __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'here.php', 'bootstrapFiles' => [ realpath(__DIR__ . '/../../../stubs/runtime/ReflectionUnionType.php'), + realpath(__DIR__ . '/../../../stubs/runtime/ReflectionAttribute.php'), realpath(__DIR__ . '/../../../stubs/runtime/Attribute.php'), + realpath(__DIR__ . '/../../../stubs/runtime/ReflectionIntersectionType.php'), __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'here.php', ], - 'autoload_files' => [ - __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'here.php', - __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'test' . DIRECTORY_SEPARATOR . 'there.php', - __DIR__ . DIRECTORY_SEPARATOR . 'up.php', - ], - 'autoload_directories' => [ - __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'src', - __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths', - realpath(__DIR__ . '/../../../') . '/conf', - ], 'scanFiles' => [ __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'here.php', __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'test' . DIRECTORY_SEPARATOR . 'there.php', @@ -196,17 +185,20 @@ public function dataParameters(): array __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'src', ], 'memoryLimitFile' => __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . '.memory_limit', - 'excludes_analyse' => [ - __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'src', - __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . '*' . DIRECTORY_SEPARATOR . 'data', - '*/src/*/data', + 'excludePaths' => [ + 'analyseAndScan' => [ + __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'src', + __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . '*' . DIRECTORY_SEPARATOR . 'data', + '*/src/*/data', + ], + 'analyse' => [], ], ], ], [ __DIR__ . '/relative-paths/nested/nested.neon', [ - 'autoload_files' => [ + 'scanFiles' => [ __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'nested' . DIRECTORY_SEPARATOR . 'here.php', __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'nested' . DIRECTORY_SEPARATOR . 'test' . DIRECTORY_SEPARATOR . 'there.php', __DIR__ . DIRECTORY_SEPARATOR . 'relative-paths' . DIRECTORY_SEPARATOR . 'up.php', @@ -296,13 +288,12 @@ public function dataParameters(): array /** * @dataProvider dataParameters - * @param string $configFile * @param array $expectedParameters - * @throws \PHPStan\Command\InceptionNotSuccessfulException + * @throws InceptionNotSuccessfulException */ public function testResolveParameters( string $configFile, - array $expectedParameters + array $expectedParameters, ): void { $result = CommandHelper::begin( @@ -311,13 +302,11 @@ public function testResolveParameters( [__DIR__], null, null, - null, [], $configFile, null, '0', false, - true ); $parameters = $result->getContainer()->getParameters(); foreach ($expectedParameters as $name => $expectedValue) { diff --git a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterIntegrationTest.php b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterIntegrationTest.php index bf62cc8e5f..07c6d9ffe2 100644 --- a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterIntegrationTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterIntegrationTest.php @@ -3,10 +3,17 @@ namespace PHPStan\Command\ErrorFormatter; use Nette\Utils\Json; +use PHPStan\ShouldNotHappenException; use PHPUnit\Framework\TestCase; +use function array_sum; use function chdir; -use function file_put_contents; +use function escapeshellarg; +use function exec; use function getcwd; +use function implode; +use function sprintf; +use function unlink; +use const PHP_BINARY; /** * @group exec @@ -24,9 +31,8 @@ public function testErrorWithTrait(): void public function testGenerateBaselineAndRunAgainWithIt(): void { - $output = $this->runPhpStan(__DIR__ . '/data/', __DIR__ . '/empty.neon', 'baselineNeon'); $baselineFile = __DIR__ . '/../../../../baseline.neon'; - file_put_contents($baselineFile, $output); + $output = $this->runPhpStan(__DIR__ . '/data/', __DIR__ . '/empty.neon', 'json', $baselineFile); $output = $this->runPhpStan(__DIR__ . '/data/', $baselineFile); @unlink($baselineFile); @@ -54,20 +60,21 @@ public function testRunUnixFileWithWindowsBaseline(): void private function runPhpStan( string $analysedPath, ?string $configFile, - string $errorFormatter = 'json' + string $errorFormatter = 'json', + ?string $baselineFile = null, ): string { $originalDir = getcwd(); if ($originalDir === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } chdir(__DIR__ . '/../../../..'); exec(sprintf('%s %s clear-result-cache %s', escapeshellarg(PHP_BINARY), 'bin/phpstan', $configFile !== null ? '--configuration ' . escapeshellarg($configFile) : ''), $clearResultCacheOutputLines, $clearResultCacheExitCode); if ($clearResultCacheExitCode !== 0) { - throw new \PHPStan\ShouldNotHappenException('Could not clear result cache.'); + throw new ShouldNotHappenException('Could not clear result cache.'); } - exec(sprintf('%s %s analyse --no-progress --error-format=%s --level=7 %s %s', escapeshellarg(PHP_BINARY), 'bin/phpstan', $errorFormatter, $configFile !== null ? '--configuration ' . escapeshellarg($configFile) : '', escapeshellarg($analysedPath)), $outputLines); + exec(sprintf('%s %s analyse --no-progress --error-format=%s --level=7 %s %s%s', escapeshellarg(PHP_BINARY), 'bin/phpstan', $errorFormatter, $configFile !== null ? '--configuration ' . escapeshellarg($configFile) : '', escapeshellarg($analysedPath), $baselineFile !== null ? ' --generate-baseline ' . escapeshellarg($baselineFile) : ''), $outputLines); chdir($originalDir); return implode("\n", $outputLines); diff --git a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php index d8a7bbe732..44d52fb724 100644 --- a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Command\ErrorFormatter; +use Generator; use Nette\Neon\Neon; use PHPStan\Analyser\Error; use PHPStan\Command\AnalysisResult; @@ -9,6 +10,7 @@ use PHPStan\Testing\ErrorFormatterTestCase; use function mt_srand; use function shuffle; +use function sprintf; use function trim; class BaselineNeonErrorFormatterTest extends ErrorFormatterTestCase @@ -100,10 +102,6 @@ public function dataFormatterOutputProvider(): iterable /** * @dataProvider dataFormatterOutputProvider * - * @param string $message - * @param int $exitCode - * @param int $numFileErrors - * @param int $numGenericErrors * @param mixed[] $expected */ public function testFormatErrors( @@ -111,14 +109,14 @@ public function testFormatErrors( int $exitCode, int $numFileErrors, int $numGenericErrors, - array $expected + array $expected, ): void { $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH)); $this->assertSame($exitCode, $formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGenericErrors), - $this->getOutput() + $this->getOutput(), ), sprintf('%s: response code do not match', $message)); $this->assertSame(trim(Neon::encode(['parameters' => ['ignoreErrors' => $expected]], Neon::BLOCK)), trim($this->getOutputContent()), sprintf('%s: output do not match', $message)); @@ -136,11 +134,11 @@ public function testFormatErrorMessagesRegexEscape(): void [], false, null, - true + true, ); $formatter->formatErrors( $result, - $this->getOutput() + $this->getOutput(), ); self::assertSame( @@ -155,9 +153,9 @@ public function testFormatErrorMessagesRegexEscape(): void ], ], ], - ], Neon::BLOCK) + ], Neon::BLOCK), ), - trim($this->getOutputContent()) + trim($this->getOutputContent()), ); } @@ -171,12 +169,12 @@ public function testEscapeDiNeon(): void [], false, null, - true + true, ); $formatter->formatErrors( $result, - $this->getOutput() + $this->getOutput(), ); self::assertSame( trim( @@ -190,16 +188,16 @@ public function testEscapeDiNeon(): void ], ], ], - ], Neon::BLOCK) + ], Neon::BLOCK), ), - trim($this->getOutputContent()) + trim($this->getOutputContent()), ); } /** - * @return \Generator}, void, void> + * @return Generator}, void, void> */ - public function outputOrderingProvider(): \Generator + public function outputOrderingProvider(): Generator { $errors = [ new Error('Error #2', 'TestfileA', 1), @@ -207,6 +205,9 @@ public function outputOrderingProvider(): \Generator new Error('Second error in a different file', 'TestfileB', 4), new Error('Error #1 in a different file', 'TestfileB', 5), new Error('Second error in a different file', 'TestfileB', 6), + new Error('Error with Windows directory separators', 'TestFiles\\TestA', 1), + new Error('Error with Unix directory separators', 'TestFiles/TestA', 1), + new Error('Error without directory separators', 'TestFilesFoo', 1), ]; yield [$errors]; mt_srand(0); @@ -230,17 +231,32 @@ public function testOutputOrdering(array $errors): void [], false, null, - true + true, ); $formatter->formatErrors( $result, - $this->getOutput() + $this->getOutput(), ); self::assertSame( trim(Neon::encode([ 'parameters' => [ 'ignoreErrors' => [ + [ + 'message' => '#^Error with Unix directory separators$#', + 'count' => 1, + 'path' => 'TestFiles/TestA', + ], + [ + 'message' => '#^Error with Windows directory separators$#', + 'count' => 1, + 'path' => 'TestFiles/TestA', + ], + [ + 'message' => '#^Error without directory separators$#', + 'count' => 1, + 'path' => 'TestFilesFoo', + ], [ 'message' => '#^A different error \\#1$#', 'count' => 1, @@ -264,7 +280,7 @@ public function testOutputOrdering(array $errors): void ], ], ], Neon::BLOCK)), - $f = trim($this->getOutputContent()) + $f = trim($this->getOutputContent()), ); } diff --git a/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php index ab868c23a8..8139e9b6ac 100644 --- a/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/CheckstyleErrorFormatterTest.php @@ -6,6 +6,7 @@ use PHPStan\Command\AnalysisResult; use PHPStan\File\SimpleRelativePathHelper; use PHPStan\Testing\ErrorFormatterTestCase; +use function sprintf; class CheckstyleErrorFormatterTest extends ErrorFormatterTestCase { @@ -112,25 +113,20 @@ public function dataFormatterOutputProvider(): iterable /** * @dataProvider dataFormatterOutputProvider * - * @param string $message - * @param int $exitCode - * @param int $numFileErrors - * @param int $numGenericErrors - * @param string $expected */ public function testFormatErrors( string $message, int $exitCode, int $numFileErrors, int $numGenericErrors, - string $expected + string $expected, ): void { $formatter = new CheckstyleErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH)); $this->assertSame($exitCode, $formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGenericErrors), - $this->getOutput() + $this->getOutput(), ), sprintf('%s: response code do not match', $message)); $outputContent = $this->getOutputContent(); @@ -147,7 +143,7 @@ public function testTraitPath(): void 5, true, __DIR__ . '/Foo.php', - __DIR__ . '/FooTrait.php' + __DIR__ . '/FooTrait.php', ); $formatter->formatErrors(new AnalysisResult( [$error], @@ -156,7 +152,7 @@ public function testTraitPath(): void [], false, null, - true + true, ), $this->getOutput()); $this->assertXmlStringEqualsXmlString(' diff --git a/tests/PHPStan/Command/ErrorFormatter/GithubErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/GithubErrorFormatterTest.php index 1b52ff5515..81d06132d2 100644 --- a/tests/PHPStan/Command/ErrorFormatter/GithubErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/GithubErrorFormatterTest.php @@ -5,6 +5,8 @@ use PHPStan\File\FuzzyRelativePathHelper; use PHPStan\File\NullRelativePathHelper; use PHPStan\Testing\ErrorFormatterTestCase; +use function sprintf; +use const PHP_VERSION_ID; class GithubErrorFormatterTest extends ErrorFormatterTestCase { @@ -27,11 +29,12 @@ public function dataFormatterOutputProvider(): iterable 1, 1, 0, - ' ------ ----------------------------------------------------------------- + ' ------ ------------------------------------------------------------------- Line folder with unicode 😃/file name with "spaces" and unicode 😃.php - ------ ----------------------------------------------------------------- + ------ ------------------------------------------------------------------- 4 Foo - ------ ----------------------------------------------------------------- + ------ ------------------------------------------------------------------- + [ERROR] Found 1 error @@ -50,6 +53,7 @@ public function dataFormatterOutputProvider(): iterable first generic error -- --------------------- + [ERROR] Found 1 error ::error ::first generic error @@ -61,13 +65,13 @@ public function dataFormatterOutputProvider(): iterable 1, 4, 0, - ' ------ ----------------------------------------------------------------- + ' ------ ------------------------------------------------------------------- Line folder with unicode 😃/file name with "spaces" and unicode 😃.php - ------ ----------------------------------------------------------------- + ------ ------------------------------------------------------------------- 2 Bar Bar2 4 Foo - ------ ----------------------------------------------------------------- + ------ ------------------------------------------------------------------- ------ --------- Line foo.php @@ -98,6 +102,7 @@ public function dataFormatterOutputProvider(): iterable second generic error -- ---------------------- + [ERROR] Found 2 errors ::error ::first generic error @@ -110,13 +115,13 @@ public function dataFormatterOutputProvider(): iterable 1, 4, 2, - ' ------ ----------------------------------------------------------------- + ' ------ ------------------------------------------------------------------- Line folder with unicode 😃/file name with "spaces" and unicode 😃.php - ------ ----------------------------------------------------------------- + ------ ------------------------------------------------------------------- 2 Bar Bar2 4 Foo - ------ ----------------------------------------------------------------- + ------ ------------------------------------------------------------------- ------ --------- Line foo.php @@ -148,29 +153,27 @@ public function dataFormatterOutputProvider(): iterable /** * @dataProvider dataFormatterOutputProvider * - * @param string $message - * @param int $exitCode - * @param int $numFileErrors - * @param int $numGenericErrors - * @param string $expected */ public function testFormatErrors( string $message, int $exitCode, int $numFileErrors, int $numGenericErrors, - string $expected + string $expected, ): void { + if (PHP_VERSION_ID >= 80100) { + self::markTestSkipped('Skipped on PHP 8.1 because of different result'); + } $relativePathHelper = new FuzzyRelativePathHelper(new NullRelativePathHelper(), self::DIRECTORY_PATH, [], '/'); $formatter = new GithubErrorFormatter( $relativePathHelper, - new TableErrorFormatter($relativePathHelper, false) + new TableErrorFormatter($relativePathHelper, false, null), ); $this->assertSame($exitCode, $formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGenericErrors), - $this->getOutput() + $this->getOutput(), ), sprintf('%s: response code do not match', $message)); $this->assertEquals($expected, $this->getOutputContent(), sprintf('%s: output do not match', $message)); diff --git a/tests/PHPStan/Command/ErrorFormatter/GitlabFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/GitlabFormatterTest.php index cadb05c53d..e4d63c28d7 100644 --- a/tests/PHPStan/Command/ErrorFormatter/GitlabFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/GitlabFormatterTest.php @@ -4,6 +4,7 @@ use PHPStan\File\SimpleRelativePathHelper; use PHPStan\Testing\ErrorFormatterTestCase; +use function sprintf; class GitlabFormatterTest extends ErrorFormatterTestCase { @@ -111,6 +112,70 @@ public function dataFormatterOutputProvider(): iterable ]', ]; + yield [ + 'Multiple file errors, including error with line=null', + 1, + 5, + 0, + '[ + { + "description": "Bar\nBar2", + "fingerprint": "034b4afbfb347494c14e396ed8327692f58be4cd27e8aff5f19f4194934db7c9", + "severity": "major", + "location": { + "path": "with space/and unicode 😃/project/folder with unicode 😃/file name with \"spaces\" and unicode 😃.php", + "lines": { + "begin": 2 + } + } + }, + { + "description": "Foo", + "fingerprint": "e82b7e1f1d4255352b19ecefa9116a12f129c7edb4351cf2319285eccdb1565e", + "severity": "major", + "location": { + "path": "with space/and unicode 😃/project/folder with unicode 😃/file name with \"spaces\" and unicode 😃.php", + "lines": { + "begin": 4 + } + } + }, + { + "description": "Bar\nBar2", + "fingerprint": "52d22d9e64bd6c6257b7a0d170ed8c99482043aeedd68c52bac081a80da9800a", + "severity": "major", + "location": { + "path": "with space/and unicode \ud83d\ude03/project/foo.php", + "lines": { + "begin": 0 + } + } + }, + { + "description": "Foo", + "fingerprint": "93c79740ed8c6fbaac2087e54d6f6f67fc0918e3ff77840530f32e19857ef63c", + "severity": "major", + "location": { + "path": "with space/and unicode \ud83d\ude03/project/foo.php", + "lines": { + "begin": 1 + } + } + }, + { + "description": "Bar\nBar2", + "fingerprint": "829f6c782152fdac840b39208c5b519d18e51bff2c601b6197812fffb8bcd9ed", + "severity": "major", + "location": { + "path": "with space/and unicode \ud83d\ude03/project/foo.php", + "lines": { + "begin": 5 + } + } + } +]', + ]; + yield [ 'Multiple generic errors', 1, @@ -221,11 +286,6 @@ public function dataFormatterOutputProvider(): iterable /** * @dataProvider dataFormatterOutputProvider * - * @param string $message - * @param int $exitCode - * @param int $numFileErrors - * @param int $numGenericErrors - * @param string $expected * */ public function testFormatErrors( @@ -233,14 +293,14 @@ public function testFormatErrors( int $exitCode, int $numFileErrors, int $numGenericErrors, - string $expected + string $expected, ): void { $formatter = new GitlabErrorFormatter(new SimpleRelativePathHelper('/data/folder')); $this->assertSame($exitCode, $formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGenericErrors), - $this->getOutput() + $this->getOutput(), ), sprintf('%s: response code do not match', $message)); $this->assertJsonStringEqualsJsonString($expected, $this->getOutputContent(), sprintf('%s: output do not match', $message)); diff --git a/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php index 48d2b2e209..d76c8c15b8 100644 --- a/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/JsonErrorFormatterTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Command\ErrorFormatter; use PHPStan\Testing\ErrorFormatterTestCase; +use function sprintf; class JsonErrorFormatterTest extends ErrorFormatterTestCase { @@ -190,25 +191,20 @@ public function dataFormatterOutputProvider(): iterable /** * @dataProvider dataFormatterOutputProvider * - * @param string $message - * @param int $exitCode - * @param int $numFileErrors - * @param int $numGenericErrors - * @param string $expected */ public function testPrettyFormatErrors( string $message, int $exitCode, int $numFileErrors, int $numGenericErrors, - string $expected + string $expected, ): void { $formatter = new JsonErrorFormatter(true); $this->assertSame($exitCode, $formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGenericErrors), - $this->getOutput() + $this->getOutput(), ), $message); $this->assertJsonStringEqualsJsonString($expected, $this->getOutputContent()); @@ -217,11 +213,6 @@ public function testPrettyFormatErrors( /** * @dataProvider dataFormatterOutputProvider * - * @param string $message - * @param int $exitCode - * @param int $numFileErrors - * @param int $numGenericErrors - * @param string $expected * */ public function testFormatErrors( @@ -229,14 +220,14 @@ public function testFormatErrors( int $exitCode, int $numFileErrors, int $numGenericErrors, - string $expected + string $expected, ): void { $formatter = new JsonErrorFormatter(false); $this->assertSame($exitCode, $formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGenericErrors), - $this->getOutput() + $this->getOutput(), ), sprintf('%s: response code do not match', $message)); $this->assertJsonStringEqualsJsonString($expected, $this->getOutputContent(), sprintf('%s: JSON do not match', $message)); diff --git a/tests/PHPStan/Command/ErrorFormatter/JunitErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/JunitErrorFormatterTest.php index cbe281f7ad..87f056da80 100644 --- a/tests/PHPStan/Command/ErrorFormatter/JunitErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/JunitErrorFormatterTest.php @@ -10,8 +10,7 @@ class JunitErrorFormatterTest extends ErrorFormatterTestCase { - /** @var \PHPStan\Command\ErrorFormatter\JunitErrorFormatter */ - private $formatter; + private JunitErrorFormatter $formatter; public function setUp(): void { @@ -21,7 +20,7 @@ public function setUp(): void } /** - * @return \Generator> + * @return Generator> */ public function dataFormatterOutputProvider(): Generator { @@ -138,16 +137,16 @@ public function testFormatErrors( int $exitCode, int $numFileErrors, int $numGeneralErrors, - string $expected + string $expected, ): void { $this->assertSame( $exitCode, $this->formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGeneralErrors), - $this->getOutput() + $this->getOutput(), ), - 'Response code do not match' + 'Response code do not match', ); $xml = new DOMDocument(); @@ -155,13 +154,13 @@ public function testFormatErrors( $this->assertTrue( $xml->schemaValidate(__DIR__ . '/junit-schema.xsd'), - 'Schema do not validate' + 'Schema do not validate', ); $this->assertXmlStringEqualsXmlString( $expected, $this->getOutputContent(), - 'XML do not match' + 'XML do not match', ); } diff --git a/tests/PHPStan/Command/ErrorFormatter/RawErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/RawErrorFormatterTest.php index 0f35ad8fda..5b61402bbd 100644 --- a/tests/PHPStan/Command/ErrorFormatter/RawErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/RawErrorFormatterTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Command\ErrorFormatter; use PHPStan\Testing\ErrorFormatterTestCase; +use function sprintf; class RawErrorFormatterTest extends ErrorFormatterTestCase { @@ -70,25 +71,20 @@ public function dataFormatterOutputProvider(): iterable /** * @dataProvider dataFormatterOutputProvider * - * @param string $message - * @param int $exitCode - * @param int $numFileErrors - * @param int $numGenericErrors - * @param string $expected */ public function testFormatErrors( string $message, int $exitCode, int $numFileErrors, int $numGenericErrors, - string $expected + string $expected, ): void { $formatter = new RawErrorFormatter(); $this->assertSame($exitCode, $formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGenericErrors), - $this->getOutput() + $this->getOutput(), ), sprintf('%s: response code do not match', $message)); $this->assertEquals($expected, $this->getOutputContent(), sprintf('%s: output do not match', $message)); diff --git a/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php index e36f959856..0e3e23acc0 100644 --- a/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php @@ -7,10 +7,18 @@ use PHPStan\File\FuzzyRelativePathHelper; use PHPStan\File\NullRelativePathHelper; use PHPStan\Testing\ErrorFormatterTestCase; +use function putenv; +use function sprintf; +use const PHP_VERSION_ID; class TableErrorFormatterTest extends ErrorFormatterTestCase { + protected function tearDown(): void + { + putenv('COLUMNS'); + } + public function dataFormatterOutputProvider(): iterable { yield [ @@ -29,11 +37,12 @@ public function dataFormatterOutputProvider(): iterable 1, 1, 0, - ' ------ ----------------------------------------------------------------- + ' ------ ------------------------------------------------------------------- Line folder with unicode 😃/file name with "spaces" and unicode 😃.php - ------ ----------------------------------------------------------------- + ------ ------------------------------------------------------------------- 4 Foo - ------ ----------------------------------------------------------------- + ------ ------------------------------------------------------------------- + [ERROR] Found 1 error @@ -51,6 +60,7 @@ public function dataFormatterOutputProvider(): iterable first generic error -- --------------------- + [ERROR] Found 1 error ', @@ -61,13 +71,13 @@ public function dataFormatterOutputProvider(): iterable 1, 4, 0, - ' ------ ----------------------------------------------------------------- + ' ------ ------------------------------------------------------------------- Line folder with unicode 😃/file name with "spaces" and unicode 😃.php - ------ ----------------------------------------------------------------- + ------ ------------------------------------------------------------------- 2 Bar Bar2 4 Foo - ------ ----------------------------------------------------------------- + ------ ------------------------------------------------------------------- ------ --------- Line foo.php @@ -94,6 +104,7 @@ public function dataFormatterOutputProvider(): iterable second generic error -- ---------------------- + [ERROR] Found 2 errors ', @@ -104,13 +115,13 @@ public function dataFormatterOutputProvider(): iterable 1, 4, 2, - ' ------ ----------------------------------------------------------------- + ' ------ ------------------------------------------------------------------- Line folder with unicode 😃/file name with "spaces" and unicode 😃.php - ------ ----------------------------------------------------------------- + ------ ------------------------------------------------------------------- 2 Bar Bar2 4 Foo - ------ ----------------------------------------------------------------- + ------ ------------------------------------------------------------------- ------ --------- Line foo.php @@ -136,25 +147,23 @@ public function dataFormatterOutputProvider(): iterable /** * @dataProvider dataFormatterOutputProvider * - * @param string $message - * @param int $exitCode - * @param int $numFileErrors - * @param int $numGenericErrors - * @param string $expected */ public function testFormatErrors( string $message, int $exitCode, int $numFileErrors, int $numGenericErrors, - string $expected + string $expected, ): void { - $formatter = new TableErrorFormatter(new FuzzyRelativePathHelper(new NullRelativePathHelper(), self::DIRECTORY_PATH, [], '/'), false); + if (PHP_VERSION_ID >= 80100) { + self::markTestSkipped('Skipped on PHP 8.1 because of different result'); + } + $formatter = new TableErrorFormatter(new FuzzyRelativePathHelper(new NullRelativePathHelper(), self::DIRECTORY_PATH, [], '/'), false, null); $this->assertSame($exitCode, $formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGenericErrors), - $this->getOutput() + $this->getOutput(), ), sprintf('%s: response code do not match', $message)); $this->assertEquals($expected, $this->getOutputContent(), sprintf('%s: output do not match', $message)); @@ -166,7 +175,32 @@ public function testEditorUrlWithTrait(): void $error = new Error('Test', 'Foo.php (in context of trait)', 12, true, 'Foo.php', 'Bar.php'); $formatter->formatErrors(new AnalysisResult([$error], [], [], [], false, null, true), $this->getOutput()); - $this->assertStringContainsString('editor://Bar.php/12', $this->getOutputContent()); + $this->assertStringContainsString('Bar.php', $this->getOutputContent()); + } + + public function testBug6727(): void + { + putenv('COLUMNS=30'); + $formatter = new TableErrorFormatter(new FuzzyRelativePathHelper(new NullRelativePathHelper(), self::DIRECTORY_PATH, [], '/'), false, null); + $formatter->formatErrors( + new AnalysisResult( + [ + new Error( + 'Method MissingTypehintPromotedProperties\Foo::__construct() has parameter $foo with no value type specified in iterable type array.', + '/var/www/html/app/src/Foo.php (in context of class App\Foo\Bar)', + 5, + ), + ], + [], + [], + [], + false, + null, + true, + ), + $this->getOutput(), + ); + self::expectNotToPerformAssertions(); } } diff --git a/tests/PHPStan/Command/ErrorFormatter/TeamcityErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/TeamcityErrorFormatterTest.php index d90ec904f6..37e7e3451c 100644 --- a/tests/PHPStan/Command/ErrorFormatter/TeamcityErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/TeamcityErrorFormatterTest.php @@ -5,6 +5,7 @@ use PHPStan\File\FuzzyRelativePathHelper; use PHPStan\File\NullRelativePathHelper; use PHPStan\Testing\ErrorFormatterTestCase; +use function sprintf; class TeamcityErrorFormatterTest extends ErrorFormatterTestCase { @@ -82,28 +83,23 @@ public function dataFormatterOutputProvider(): iterable /** * @dataProvider dataFormatterOutputProvider * - * @param string $message - * @param int $exitCode - * @param int $numFileErrors - * @param int $numGenericErrors - * @param string $expected */ public function testFormatErrors( string $message, int $exitCode, int $numFileErrors, int $numGenericErrors, - string $expected + string $expected, ): void { $relativePathHelper = new FuzzyRelativePathHelper(new NullRelativePathHelper(), self::DIRECTORY_PATH, [], '/'); $formatter = new TeamcityErrorFormatter( - $relativePathHelper + $relativePathHelper, ); $this->assertSame($exitCode, $formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGenericErrors), - $this->getOutput() + $this->getOutput(), ), sprintf('%s: response code do not match', $message)); $this->assertEquals($expected, $this->getOutputContent(), sprintf('%s: output do not match', $message)); diff --git a/tests/PHPStan/Command/ErrorFormatter/data/unixBaseline.neon b/tests/PHPStan/Command/ErrorFormatter/data/unixBaseline.neon index df99f564ae..ca7c8f9c2c 100644 --- a/tests/PHPStan/Command/ErrorFormatter/data/unixBaseline.neon +++ b/tests/PHPStan/Command/ErrorFormatter/data/unixBaseline.neon @@ -1,12 +1,12 @@ parameters: ignoreErrors: - - message: "#^Method BaselineIntegration\\\\WindowsNewlines\\:\\:phpdocWithNewlines\\(\\) has no return typehint specified\\.$#" + message: "#^Method BaselineIntegration\\\\WindowsNewlines\\:\\:phpdocWithNewlines\\(\\) has no return type specified\\.$#" count: 1 path: WindowsNewlines.php - - message: "#^Method BaselineIntegration\\\\WindowsNewlines\\:\\:phpdocWithNewlines\\(\\) has parameter \\$object with no typehint specified\\.$#" + message: "#^Method BaselineIntegration\\\\WindowsNewlines\\:\\:phpdocWithNewlines\\(\\) has parameter \\$object with no type specified\\.$#" count: 1 path: WindowsNewlines.php diff --git a/tests/PHPStan/Command/ErrorFormatter/data/windowsBaseline.neon b/tests/PHPStan/Command/ErrorFormatter/data/windowsBaseline.neon index b914b6553f..3bfe998b6e 100644 --- a/tests/PHPStan/Command/ErrorFormatter/data/windowsBaseline.neon +++ b/tests/PHPStan/Command/ErrorFormatter/data/windowsBaseline.neon @@ -1,12 +1,12 @@ parameters: ignoreErrors: - - message: "#^Method BaselineIntegration\\\\UnixNewlines\\:\\:phpdocWithNewlines\\(\\) has no return typehint specified\\.$#" + message: "#^Method BaselineIntegration\\\\UnixNewlines\\:\\:phpdocWithNewlines\\(\\) has no return type specified\\.$#" count: 1 path: UnixNewlines.php - - message: "#^Method BaselineIntegration\\\\UnixNewlines\\:\\:phpdocWithNewlines\\(\\) has parameter \\$object with no typehint specified\\.$#" + message: "#^Method BaselineIntegration\\\\UnixNewlines\\:\\:phpdocWithNewlines\\(\\) has parameter \\$object with no type specified\\.$#" count: 1 path: UnixNewlines.php diff --git a/tests/PHPStan/Command/IgnoredRegexValidatorTest.php b/tests/PHPStan/Command/IgnoredRegexValidatorTest.php index 0220251751..9dff05c44f 100644 --- a/tests/PHPStan/Command/IgnoredRegexValidatorTest.php +++ b/tests/PHPStan/Command/IgnoredRegexValidatorTest.php @@ -2,10 +2,12 @@ namespace PHPStan\Command; +use Hoa\Compiler\Llk\Llk; +use Hoa\File\Read; use PHPStan\PhpDoc\TypeStringResolver; -use PHPStan\Testing\TestCase; +use PHPStan\Testing\PHPStanTestCase; -class IgnoredRegexValidatorTest extends TestCase +class IgnoredRegexValidatorTest extends PHPStanTestCase { public function dataValidate(): array @@ -115,20 +117,17 @@ public function dataValidate(): array /** * @dataProvider dataValidate - * @param string $regex * @param string[] $expectedTypes - * @param bool $expectedHasAnchors - * @param bool $expectAllErrorsIgnored */ public function testValidate( string $regex, array $expectedTypes, bool $expectedHasAnchors, - bool $expectAllErrorsIgnored + bool $expectAllErrorsIgnored, ): void { - $grammar = new \Hoa\File\Read('hoa://Library/Regex/Grammar.pp'); - $parser = \Hoa\Compiler\Llk\Llk::load($grammar); + $grammar = new Read('hoa://Library/Regex/Grammar.pp'); + $parser = Llk::load($grammar); $validator = new IgnoredRegexValidator($parser, self::getContainer()->getByType(TypeStringResolver::class)); $result = $validator->validate($regex); diff --git a/tests/PHPStan/Command/relative-paths/nested/nested.neon b/tests/PHPStan/Command/relative-paths/nested/nested.neon index c18dd719f7..6e097edf21 100644 --- a/tests/PHPStan/Command/relative-paths/nested/nested.neon +++ b/tests/PHPStan/Command/relative-paths/nested/nested.neon @@ -1,5 +1,5 @@ parameters: - autoload_files: + scanFiles: - here.php - test/there.php - ../up.php diff --git a/tests/PHPStan/Command/relative-paths/root.neon b/tests/PHPStan/Command/relative-paths/root.neon index 138d7e341c..834bcfd001 100644 --- a/tests/PHPStan/Command/relative-paths/root.neon +++ b/tests/PHPStan/Command/relative-paths/root.neon @@ -1,15 +1,6 @@ parameters: - bootstrap: here.php bootstrapFiles: - here.php - autoload_files: - - here.php - - test/there.php - - ../up.php - autoload_directories: - - src - - . - - %rootDir%/conf scanFiles: - here.php - test/there.php @@ -21,7 +12,7 @@ parameters: paths: - src memoryLimitFile: .memory_limit - excludes_analyse: + excludePaths: - src - src/*/data - */src/*/data diff --git a/tests/PHPStan/Command/test-autodiscover-dist-dot-neon/phpstan.dist.neon b/tests/PHPStan/Command/test-autodiscover-dist-dot-neon/phpstan.dist.neon new file mode 100644 index 0000000000..f242b77eeb --- /dev/null +++ b/tests/PHPStan/Command/test-autodiscover-dist-dot-neon/phpstan.dist.neon @@ -0,0 +1,4 @@ +includes: + - ../../../../conf/bleedingEdge.neon + +parameters: diff --git a/tests/PHPStan/Command/test-autodiscover-priority-dist-dot-neon/phpstan.dist.neon b/tests/PHPStan/Command/test-autodiscover-priority-dist-dot-neon/phpstan.dist.neon new file mode 100644 index 0000000000..f242b77eeb --- /dev/null +++ b/tests/PHPStan/Command/test-autodiscover-priority-dist-dot-neon/phpstan.dist.neon @@ -0,0 +1,4 @@ +includes: + - ../../../../conf/bleedingEdge.neon + +parameters: diff --git a/tests/PHPStan/Command/test-autodiscover-priority-dist-dot-neon/phpstan.neon b/tests/PHPStan/Command/test-autodiscover-priority-dist-dot-neon/phpstan.neon new file mode 100644 index 0000000000..f242b77eeb --- /dev/null +++ b/tests/PHPStan/Command/test-autodiscover-priority-dist-dot-neon/phpstan.neon @@ -0,0 +1,4 @@ +includes: + - ../../../../conf/bleedingEdge.neon + +parameters: diff --git a/tests/PHPStan/Composer/AutoloadFilesTest.php b/tests/PHPStan/Composer/AutoloadFilesTest.php index d325896e0d..ca29b7395b 100644 --- a/tests/PHPStan/Composer/AutoloadFilesTest.php +++ b/tests/PHPStan/Composer/AutoloadFilesTest.php @@ -5,8 +5,19 @@ use Nette\Utils\Json; use PHPStan\File\FileHelper; use PHPStan\File\FileReader; +use PHPStan\ShouldNotHappenException; use PHPUnit\Framework\TestCase; use Symfony\Component\Finder\Finder; +use function array_map; +use function array_splice; +use function dirname; +use function realpath; +use function sort; +use function strlen; +use function strpos; +use function substr; +use const DIRECTORY_SEPARATOR; +use const PHP_VERSION_ID; class AutoloadFilesTest extends TestCase { @@ -18,7 +29,7 @@ public function testExpectedFiles(): void $autoloadFiles = []; $vendorPath = realpath(__DIR__ . '/../../../vendor'); if ($vendorPath === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $fileHelper = new FileHelper(__DIR__); @@ -26,7 +37,7 @@ public function testExpectedFiles(): void foreach ($finder->files()->name('composer.json')->in(__DIR__ . '/../../../vendor') as $fileInfo) { $realpath = $fileInfo->getRealPath(); if ($realpath === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $json = Json::decode(FileReader::read($realpath), Json::FORCE_ARRAY); if (!isset($json['autoload']['files'])) { @@ -34,7 +45,10 @@ public function testExpectedFiles(): void } foreach ($json['autoload']['files'] as $file) { - $autoloadFile = substr(dirname($realpath) . '/' . $file, strlen($vendorPath) + 1); + $autoloadFile = substr(dirname($realpath) . DIRECTORY_SEPARATOR . $file, strlen($vendorPath) + 1); + if (strpos($autoloadFile, 'rector' . DIRECTORY_SEPARATOR . 'rector' . DIRECTORY_SEPARATOR) === 0) { + continue; + } $autoloadFiles[] = $fileHelper->normalizePath($autoloadFile); } } @@ -52,10 +66,16 @@ public function testExpectedFiles(): void '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 + 'symfony/deprecation-contracts/function.php', // afaik polyfills aren't necessary 'symfony/polyfill-ctype/bootstrap.php', // afaik polyfills aren't necessary + '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/string/Resources/functions.php', // afaik polyfills aren't necessary ]; $phpunitFunctions = 'phpunit/phpunit/src/Framework/Assert/Functions.php'; @@ -65,9 +85,7 @@ public function testExpectedFiles(): void ]); } - $expectedFiles = array_map(static function (string $path) use ($fileHelper): string { - return $fileHelper->normalizePath($path); - }, $expectedFiles); + $expectedFiles = array_map(static fn (string $path): string => $fileHelper->normalizePath($path), $expectedFiles); sort($expectedFiles); $this->assertSame($expectedFiles, $autoloadFiles); diff --git a/tests/PHPStan/Dependency/DependencyDumperTest.php b/tests/PHPStan/Dependency/DependencyDumperTest.php deleted file mode 100644 index 7ed6cff27b..0000000000 --- a/tests/PHPStan/Dependency/DependencyDumperTest.php +++ /dev/null @@ -1,145 +0,0 @@ -getByType(NodeScopeResolver::class); - - /** @var Parser $realParser */ - $realParser = $container->getByType(Parser::class); - - $mockParser = $this->createMock(Parser::class); - $mockParser->method('parseFile') - ->willReturnCallback(static function (string $file) use ($realParser): array { - if (file_exists($file)) { - return $realParser->parseFile($file); - } - - return []; - }); - - /** @var Broker $realBroker */ - $realBroker = $container->getByType(Broker::class); - - $fileHelper = new FileHelper(__DIR__); - - $mockBroker = $this->createMock(Broker::class); - $mockBroker->method('getClass') - ->willReturnCallback(function (string $class) use ($realBroker, $fileHelper): ClassReflection { - if (in_array($class, [ - GrandChild::class, - Child::class, - ParentClass::class, - ], true)) { - return $realBroker->getClass($class); - } - - $nameParts = explode('\\', $class); - $shortClass = array_pop($nameParts); - - $classReflection = $this->createMock(ClassReflection::class); - $classReflection->method('getInterfaces')->willReturn([]); - $classReflection->method('getTraits')->willReturn([]); - $classReflection->method('getParentClass')->willReturn(false); - $classReflection->method('getFilename')->willReturn( - $fileHelper->normalizePath(__DIR__ . '/data/' . $shortClass . '.php') - ); - - return $classReflection; - }); - - $expectedDependencyTree = $this->getExpectedDependencyTree($fileHelper); - - /** @var ScopeFactory $scopeFactory */ - $scopeFactory = $container->getByType(ScopeFactory::class); - - /** @var FileFinder $fileFinder */ - $fileFinder = $container->getService('fileFinderAnalyse'); - - $dumper = new DependencyDumper( - new DependencyResolver($fileHelper, $mockBroker, new ExportedNodeResolver(self::getContainer()->getByType(FileTypeMapper::class), new Standard())), - $nodeScopeResolver, - $mockParser, - $scopeFactory, - $fileFinder - ); - - $dependencies = $dumper->dumpDependencies( - array_merge( - [$fileHelper->normalizePath(__DIR__ . '/data/GrandChild.php')], - array_keys($expectedDependencyTree) - ), - static function (): void { - }, - static function (): void { - }, - null - ); - - $this->assertCount(count($expectedDependencyTree), $dependencies); - foreach ($expectedDependencyTree as $file => $files) { - $this->assertArrayHasKey($file, $dependencies); - $this->assertSame($files, $dependencies[$file]); - } - } - - /** - * @param FileHelper $fileHelper - * @return string[][] - */ - private function getExpectedDependencyTree(FileHelper $fileHelper): array - { - $tree = [ - 'Child.php' => [ - 'GrandChild.php', - ], - 'Parent.php' => [ - 'GrandChild.php', - 'Child.php', - ], - 'MethodNativeReturnTypehint.php' => [ - 'GrandChild.php', - ], - 'MethodPhpDocReturnTypehint.php' => [ - 'GrandChild.php', - ], - 'ParamNativeReturnTypehint.php' => [ - 'GrandChild.php', - ], - 'ParamPhpDocReturnTypehint.php' => [ - 'GrandChild.php', - ], - ]; - - $expectedTree = []; - foreach ($tree as $file => $files) { - $expectedTree[$fileHelper->normalizePath(__DIR__ . '/data/' . $file)] = array_map(static function (string $file) use ($fileHelper): string { - return $fileHelper->normalizePath(__DIR__ . '/data/' . $file); - }, $files); - } - - return $expectedTree; - } - -} diff --git a/tests/PHPStan/Dependency/data/Child.php b/tests/PHPStan/Dependency/data/Child.php deleted file mode 100644 index 8e9e8a3495..0000000000 --- a/tests/PHPStan/Dependency/data/Child.php +++ /dev/null @@ -1,8 +0,0 @@ -skipIfNotOnWindows(); @@ -117,14 +117,12 @@ public function dataExcludeOnWindows(): array /** * @dataProvider dataExcludeOnUnix - * @param string $filePath * @param string[] $analyseExcludes - * @param bool $isExcluded */ public function testFilesAreExcludedFromAnalysingOnUnix( string $filePath, array $analyseExcludes, - bool $isExcluded + bool $isExcluded, ): void { $this->skipIfNotOnUnix(); diff --git a/tests/PHPStan/File/FileHelperTest.php b/tests/PHPStan/File/FileHelperTest.php index 9e2d88ec8d..5cc0941fd7 100644 --- a/tests/PHPStan/File/FileHelperTest.php +++ b/tests/PHPStan/File/FileHelperTest.php @@ -2,7 +2,9 @@ namespace PHPStan\File; -class FileHelperTest extends \PHPStan\Testing\TestCase +use PHPStan\Testing\PHPStanTestCase; + +class FileHelperTest extends PHPStanTestCase { /** @@ -23,8 +25,6 @@ public function dataAbsolutizePathOnWindows(): array /** * @dataProvider dataAbsolutizePathOnWindows - * @param string $path - * @param string $absolutePath */ public function testAbsolutizePathOnWindows(string $path, string $absolutePath): void { @@ -52,8 +52,6 @@ public function dataAbsolutizePathOnLinuxOrMac(): array /** * @dataProvider dataAbsolutizePathOnLinuxOrMac - * @param string $path - * @param string $absolutePath */ public function testAbsolutizePathOnLinuxOrMac(string $path, string $absolutePath): void { @@ -80,8 +78,6 @@ public function dataNormalizePathOnWindows(): array /** * @dataProvider dataNormalizePathOnWindows - * @param string $path - * @param string $normalizedPath */ public function testNormalizePathOnWindows(string $path, string $normalizedPath): void { @@ -109,8 +105,6 @@ public function dataNormalizePathOnLinuxOrMac(): array /** * @dataProvider dataNormalizePathOnLinuxOrMac - * @param string $path - * @param string $normalizedPath */ public function testNormalizePathOnLinuxOrMac(string $path, string $normalizedPath): void { diff --git a/tests/PHPStan/File/ParentDirectoryRelativePathHelperTest.php b/tests/PHPStan/File/ParentDirectoryRelativePathHelperTest.php index bd0cb74822..d4065efb28 100644 --- a/tests/PHPStan/File/ParentDirectoryRelativePathHelperTest.php +++ b/tests/PHPStan/File/ParentDirectoryRelativePathHelperTest.php @@ -105,20 +105,17 @@ public function dataGetRelativePath(): array /** * @dataProvider dataGetRelativePath - * @param string $parentDirectory - * @param string $filename - * @param string $expectedRelativePath */ public function testGetRelativePath( string $parentDirectory, string $filename, - string $expectedRelativePath + string $expectedRelativePath, ): void { $helper = new ParentDirectoryRelativePathHelper($parentDirectory); $this->assertSame( $expectedRelativePath, - $helper->getRelativePath($filename) + $helper->getRelativePath($filename), ); } diff --git a/tests/PHPStan/File/RelativePathHelperTest.php b/tests/PHPStan/File/RelativePathHelperTest.php index 7ede64384d..5dcb549b8d 100644 --- a/tests/PHPStan/File/RelativePathHelperTest.php +++ b/tests/PHPStan/File/RelativePathHelperTest.php @@ -2,7 +2,12 @@ namespace PHPStan\File; -class RelativePathHelperTest extends \PHPUnit\Framework\TestCase +use PHPUnit\Framework\TestCase; +use function array_map; +use function str_replace; +use function substr; + +class RelativePathHelperTest extends TestCase { public function dataGetRelativePath(): array @@ -142,37 +147,31 @@ public function dataGetRelativePath(): array /** * @dataProvider dataGetRelativePath - * @param string $currentWorkingDirectory * @param string[] $analysedPaths - * @param string $filenameToRelativize - * @param string $expectedResult */ public function testGetRelativePathOnUnix( string $currentWorkingDirectory, array $analysedPaths, string $filenameToRelativize, - string $expectedResult + string $expectedResult, ): void { $helper = new FuzzyRelativePathHelper(new NullRelativePathHelper(), $currentWorkingDirectory, $analysedPaths, '/'); $this->assertSame( $expectedResult, - $helper->getRelativePath($filenameToRelativize) + $helper->getRelativePath($filenameToRelativize), ); } /** * @dataProvider dataGetRelativePath - * @param string $currentWorkingDirectory * @param string[] $analysedPaths - * @param string $filenameToRelativize - * @param string $expectedResult */ public function testGetRelativePathOnWindows( string $currentWorkingDirectory, array $analysedPaths, string $filenameToRelativize, - string $expectedResult + string $expectedResult, ): void { $sanitize = static function (string $path): string { @@ -185,7 +184,7 @@ public function testGetRelativePathOnWindows( $helper = new FuzzyRelativePathHelper(new NullRelativePathHelper(), $sanitize($currentWorkingDirectory), array_map($sanitize, $analysedPaths), '\\'); $this->assertSame( $sanitize($expectedResult), - $helper->getRelativePath($sanitize($filenameToRelativize)) + $helper->getRelativePath($sanitize($filenameToRelativize)), ); } @@ -215,22 +214,19 @@ public function dataGetRelativePathWindowsSpecific(): array /** * @dataProvider dataGetRelativePathWindowsSpecific - * @param string $currentWorkingDirectory * @param string[] $analysedPaths - * @param string $filenameToRelativize - * @param string $expectedResult */ public function testGetRelativePathWindowsSpecific( string $currentWorkingDirectory, array $analysedPaths, string $filenameToRelativize, - string $expectedResult + string $expectedResult, ): void { $helper = new FuzzyRelativePathHelper(new NullRelativePathHelper(), $currentWorkingDirectory, $analysedPaths, '\\'); $this->assertSame( $expectedResult, - $helper->getRelativePath($filenameToRelativize) + $helper->getRelativePath($filenameToRelativize), ); } diff --git a/tests/PHPStan/Fixture/AnotherTestEnum.php b/tests/PHPStan/Fixture/AnotherTestEnum.php new file mode 100644 index 0000000000..0a6352ae4f --- /dev/null +++ b/tests/PHPStan/Fixture/AnotherTestEnum.php @@ -0,0 +1,12 @@ += 8.1 + +namespace PHPStan\Fixture; + +enum AnotherTestEnum: int +{ + + case ONE = 1; + case TWO = 2; + const CONST_ONE = 1; + +} diff --git a/tests/PHPStan/Fixture/FinalClass.php b/tests/PHPStan/Fixture/FinalClass.php new file mode 100644 index 0000000000..43d05f45ee --- /dev/null +++ b/tests/PHPStan/Fixture/FinalClass.php @@ -0,0 +1,8 @@ += 8.1 + +namespace PHPStan\Fixture; + +enum TestEnum: int implements TestEnumInterface +{ + + case ONE = 1; + case TWO = 2; + const CONST_ONE = 1; + +} diff --git a/tests/PHPStan/Fixture/TestEnumInterface.php b/tests/PHPStan/Fixture/TestEnumInterface.php new file mode 100644 index 0000000000..c26b4d28d3 --- /dev/null +++ b/tests/PHPStan/Fixture/TestEnumInterface.php @@ -0,0 +1,8 @@ += 8.1 + +namespace PHPStan\Fixture; + +interface TestEnumInterface +{ + +} diff --git a/tests/PHPStan/Generics/GenericsIntegrationTest.php b/tests/PHPStan/Generics/GenericsIntegrationTest.php index c637d3fd3a..e88e973b8f 100644 --- a/tests/PHPStan/Generics/GenericsIntegrationTest.php +++ b/tests/PHPStan/Generics/GenericsIntegrationTest.php @@ -2,10 +2,12 @@ namespace PHPStan\Generics; +use PHPStan\Testing\LevelsTestCase; + /** * @group exec */ -class GenericsIntegrationTest extends \PHPStan\Testing\LevelsTestCase +class GenericsIntegrationTest extends LevelsTestCase { public function dataTopics(): array @@ -22,6 +24,7 @@ public function dataTopics(): array ['bug2620'], ['bug2622'], ['bug2627'], + ['bug6210'], ]; } diff --git a/tests/PHPStan/Generics/TemplateTypeFactoryTest.php b/tests/PHPStan/Generics/TemplateTypeFactoryTest.php index cea4dd2da3..16fe1d24df 100644 --- a/tests/PHPStan/Generics/TemplateTypeFactoryTest.php +++ b/tests/PHPStan/Generics/TemplateTypeFactoryTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Generics; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; @@ -13,8 +14,9 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function sprintf; -class TemplateTypeFactoryTest extends \PHPStan\Testing\TestCase +class TemplateTypeFactoryTest extends PHPStanTestCase { /** @return array */ @@ -50,7 +52,7 @@ public function dataCreate(): array TemplateTypeScope::createWithFunction('a'), 'U', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new MixedType(), ], @@ -77,12 +79,12 @@ public function testCreate(?Type $bound, Type $expectedBound): void $scope, 'T', $bound, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ); $this->assertTrue( $expectedBound->equals($templateType->getBound()), - sprintf('%s -> equals(%s)', $expectedBound->describe(VerbosityLevel::precise()), $templateType->getBound()->describe(VerbosityLevel::precise())) + sprintf('%s -> equals(%s)', $expectedBound->describe(VerbosityLevel::precise()), $templateType->getBound()->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Generics/data/bug6210.php b/tests/PHPStan/Generics/data/bug6210.php new file mode 100644 index 0000000000..5a4202fd03 --- /dev/null +++ b/tests/PHPStan/Generics/data/bug6210.php @@ -0,0 +1,29 @@ +show($entity); + } + + /** + * @phpstan-param TEntityClass $entity + */ + private function show($entity): void + { + } +} diff --git a/tests/PHPStan/Generics/data/classes-3.json b/tests/PHPStan/Generics/data/classes-7.json similarity index 100% rename from tests/PHPStan/Generics/data/classes-3.json rename to tests/PHPStan/Generics/data/classes-7.json diff --git a/tests/PHPStan/Generics/data/functions-6.json b/tests/PHPStan/Generics/data/functions-6.json index 00b36cab69..72e5278a64 100644 --- a/tests/PHPStan/Generics/data/functions-6.json +++ b/tests/PHPStan/Generics/data/functions-6.json @@ -1,6 +1,6 @@ [ { - "message": "Function PHPStan\\Generics\\Functions\\testF() has no return typehint specified.", + "message": "Function PHPStan\\Generics\\Functions\\testF() has no return type specified.", "line": 27, "ignorable": true }, diff --git a/tests/PHPStan/Generics/data/invalidReturn-3.json b/tests/PHPStan/Generics/data/invalidReturn-7.json similarity index 100% rename from tests/PHPStan/Generics/data/invalidReturn-3.json rename to tests/PHPStan/Generics/data/invalidReturn-7.json diff --git a/tests/PHPStan/Generics/data/pick-6.json b/tests/PHPStan/Generics/data/pick-6.json index 4b5aef0bd0..1598be5320 100644 --- a/tests/PHPStan/Generics/data/pick-6.json +++ b/tests/PHPStan/Generics/data/pick-6.json @@ -1,6 +1,6 @@ [ { - "message": "Function PHPStan\\Generics\\Pick\\test() has no return typehint specified.", + "message": "Function PHPStan\\Generics\\Pick\\test() has no return type specified.", "line": 22, "ignorable": true } diff --git a/tests/PHPStan/Generics/data/varyingAcceptor-6.json b/tests/PHPStan/Generics/data/varyingAcceptor-6.json index cb26b5d815..72f7b7dc7a 100644 --- a/tests/PHPStan/Generics/data/varyingAcceptor-6.json +++ b/tests/PHPStan/Generics/data/varyingAcceptor-6.json @@ -1,6 +1,6 @@ [ { - "message": "Function PHPStan\\Generics\\VaryingAcceptor\\testApply() has no return typehint specified.", + "message": "Function PHPStan\\Generics\\VaryingAcceptor\\testApply() has no return type specified.", "line": 30, "ignorable": true } diff --git a/tests/PHPStan/Levels/InferPrivatePropertyTypeFromConstructorIntegrationTest.php b/tests/PHPStan/Levels/InferPrivatePropertyTypeFromConstructorIntegrationTest.php index c3dacc310b..5a7b686525 100644 --- a/tests/PHPStan/Levels/InferPrivatePropertyTypeFromConstructorIntegrationTest.php +++ b/tests/PHPStan/Levels/InferPrivatePropertyTypeFromConstructorIntegrationTest.php @@ -2,10 +2,12 @@ namespace PHPStan\Levels; +use PHPStan\Testing\LevelsTestCase; + /** * @group exec */ -class InferPrivatePropertyTypeFromConstructorIntegrationTest extends \PHPStan\Testing\LevelsTestCase +class InferPrivatePropertyTypeFromConstructorIntegrationTest extends LevelsTestCase { public function dataTopics(): array diff --git a/tests/PHPStan/Levels/LevelsCheckAlwaysTrueIntegrationTest.php b/tests/PHPStan/Levels/LevelsCheckAlwaysTrueIntegrationTest.php index 0230eddb6b..ec322d6b09 100644 --- a/tests/PHPStan/Levels/LevelsCheckAlwaysTrueIntegrationTest.php +++ b/tests/PHPStan/Levels/LevelsCheckAlwaysTrueIntegrationTest.php @@ -2,10 +2,12 @@ namespace PHPStan\Levels; +use PHPStan\Testing\LevelsTestCase; + /** * @group exec */ -class LevelsCheckAlwaysTrueIntegrationTest extends \PHPStan\Testing\LevelsTestCase +class LevelsCheckAlwaysTrueIntegrationTest extends LevelsTestCase { public function dataTopics(): array diff --git a/tests/PHPStan/Levels/LevelsIntegrationTest.php b/tests/PHPStan/Levels/LevelsIntegrationTest.php index f70127daea..4badfafef3 100644 --- a/tests/PHPStan/Levels/LevelsIntegrationTest.php +++ b/tests/PHPStan/Levels/LevelsIntegrationTest.php @@ -2,10 +2,12 @@ namespace PHPStan\Levels; +use PHPStan\Testing\LevelsTestCase; + /** * @group exec */ -class LevelsIntegrationTest extends \PHPStan\Testing\LevelsTestCase +class LevelsIntegrationTest extends LevelsTestCase { public function dataTopics(): array diff --git a/tests/PHPStan/Levels/NamedArgumentsIntegrationTest.php b/tests/PHPStan/Levels/NamedArgumentsIntegrationTest.php index 6f7aaf4dad..bf14a77c9e 100644 --- a/tests/PHPStan/Levels/NamedArgumentsIntegrationTest.php +++ b/tests/PHPStan/Levels/NamedArgumentsIntegrationTest.php @@ -2,10 +2,12 @@ namespace PHPStan\Levels; +use PHPStan\Testing\LevelsTestCase; + /** * @group exec */ -class NamedArgumentsIntegrationTest extends \PHPStan\Testing\LevelsTestCase +class NamedArgumentsIntegrationTest extends LevelsTestCase { public function dataTopics(): array diff --git a/tests/PHPStan/Levels/StubValidatorIntegrationTest.php b/tests/PHPStan/Levels/StubValidatorIntegrationTest.php index 80d171f7db..1c4dcf3142 100644 --- a/tests/PHPStan/Levels/StubValidatorIntegrationTest.php +++ b/tests/PHPStan/Levels/StubValidatorIntegrationTest.php @@ -2,10 +2,12 @@ namespace PHPStan\Levels; +use PHPStan\Testing\LevelsTestCase; + /** * @group exec */ -class StubValidatorIntegrationTest extends \PHPStan\Testing\LevelsTestCase +class StubValidatorIntegrationTest extends LevelsTestCase { public function dataTopics(): array diff --git a/tests/PHPStan/Levels/StubsIntegrationTest.php b/tests/PHPStan/Levels/StubsIntegrationTest.php index d157ec70e1..11ad056599 100644 --- a/tests/PHPStan/Levels/StubsIntegrationTest.php +++ b/tests/PHPStan/Levels/StubsIntegrationTest.php @@ -2,10 +2,12 @@ namespace PHPStan\Levels; +use PHPStan\Testing\LevelsTestCase; + /** * @group exec */ -class StubsIntegrationTest extends \PHPStan\Testing\LevelsTestCase +class StubsIntegrationTest extends LevelsTestCase { public function dataTopics(): array diff --git a/tests/PHPStan/Levels/data/acceptTypes-5.json b/tests/PHPStan/Levels/data/acceptTypes-5.json index 5e4de19fb5..c9f5fdf981 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-5.json +++ b/tests/PHPStan/Levels/data/acceptTypes-5.json @@ -60,7 +60,7 @@ "ignorable": true }, { - "message": "Parameter #1 $var of function count expects array|Countable, string given.", + "message": "Parameter #1 $value of function count expects array|Countable, string given.", "line": 170, "ignorable": true }, @@ -140,27 +140,27 @@ "ignorable": true }, { - "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array('foo' => callable(): mixed), array given.", + "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array given.", "line": 579, "ignorable": true }, { - "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array('foo' => callable(): mixed), array() given.", + "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array{} given.", "line": 580, "ignorable": true }, { - "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array('foo' => callable(): mixed), array('foo' => 1) given.", + "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array{foo: 1} given.", "line": 582, "ignorable": true }, { - "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array('foo' => callable(): mixed), array('foo' => 'nonexistent') given.", + "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array{foo: 'nonexistent'} given.", "line": 584, "ignorable": true }, { - "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array('foo' => callable(): mixed), array('bar' => 'date') given.", + "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array{bar: 'date'} given.", "line": 585, "ignorable": true }, @@ -180,23 +180,28 @@ "ignorable": true }, { - "message": "Parameter #1 $numericString of method Levels\\AcceptTypes\\NumericStrings::doBar() expects string&numeric, 'foo' given.", + "message": "Parameter #1 $numericString of method Levels\\AcceptTypes\\NumericStrings::doBar() expects numeric-string, 'foo' given.", "line": 707, "ignorable": true }, { - "message": "Parameter #1 $numericString of method Levels\\AcceptTypes\\NumericStrings::doBar() expects string&numeric, string given.", + "message": "Parameter #1 $numericString of method Levels\\AcceptTypes\\NumericStrings::doBar() expects numeric-string, string given.", "line": 708, "ignorable": true }, { - "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects array&nonEmpty, array() given.", + "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects non-empty-array, array{} given.", "line": 733, "ignorable": true }, { - "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects array&nonEmpty, array given.", + "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects non-empty-array, array given.", "line": 735, "ignorable": true + }, + { + "message": "Parameter #2 $array of function implode expects array|null, int given.", + "line": 763, + "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/acceptTypes-6.json b/tests/PHPStan/Levels/data/acceptTypes-6.json index 543bf6620a..35bd142bb9 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-6.json +++ b/tests/PHPStan/Levels/data/acceptTypes-6.json @@ -1,152 +1,167 @@ [ { - "message": "Method Levels\\AcceptTypes\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Foo::doFoo() has no return type specified.", "line": 15, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Foo::doBar() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Foo::doBar() has no return type specified.", "line": 30, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Foo::doBaz() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Foo::doBaz() has no return type specified.", "line": 41, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Foo::doLorem() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Foo::doLorem() has no return type specified.", "line": 60, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Foo::doIpsum() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Foo::doIpsum() has no return type specified.", "line": 68, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Foo::doFooArray() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Foo::doFooArray() has no return type specified.", "line": 80, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Foo::doBarArray() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Foo::doBarArray() has no return type specified.", "line": 98, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Foo::doBazArray() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Foo::doBazArray() has no return type specified.", "line": 103, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Foo::doBazArrayUnionItemTypes() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Foo::doBazArrayUnionItemTypes() has no return type specified.", "line": 127, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Foo::callableArray() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Foo::callableArray() has no return type specified.", "line": 138, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Foo::expectCallable() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Foo::expectCallable() has no return type specified.", "line": 148, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Foo::iterableCountable() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Foo::iterableCountable() has no return type specified.", "line": 160, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Foo::benevolentUnionNotReported() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Foo::benevolentUnionNotReported() has no return type specified.", "line": 176, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\ClosureAccepts::doFoo() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\ClosureAccepts::doFoo() has no return type specified.", "line": 208, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\ClosureAccepts::doFooUnionClosures() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\ClosureAccepts::doFooUnionClosures() has no return type specified.", "line": 254, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\ClosureAccepts::doBar() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\ClosureAccepts::doBar() has no return type specified.", "line": 332, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\ClosureAccepts::doBaz() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\ClosureAccepts::doBaz() has no return type specified.", "line": 342, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Baz::doFoo() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Baz::doFoo() has no return type specified.", "line": 409, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Baz::doBar() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Baz::doBar() has no return type specified.", "line": 418, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Baz::doBaz() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Baz::doBaz() has no return type specified.", "line": 423, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Baz::doLorem() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Baz::doLorem() has no return type specified.", "line": 437, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Baz::doIpsum() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Baz::doIpsum() has no return type specified.", "line": 445, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Baz::doFooArray() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Baz::doFooArray() has no return type specified.", "line": 490, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Baz::doBarArray() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Baz::doBarArray() has no return type specified.", "line": 502, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Baz::testUnions() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Baz::testUnions() has no return type specified.", "line": 524, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Baz::testUnions2() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Baz::testUnions2() has no return type specified.", "line": 535, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Baz::requireArray() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Baz::requireArray() has no return type specified.", "line": 549, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\Baz::requireFoo() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\Baz::requireFoo() has no return type specified.", "line": 554, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\ArrayShapes::doFoo() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\ArrayShapes::doFoo() has no return type specified.", "line": 570, "ignorable": true }, { - "message": "Method Levels\\AcceptTypes\\ArrayShapes::doBar() has no return typehint specified.", + "message": "Method Levels\\AcceptTypes\\ArrayShapes::doBar() has no return type specified.", "line": 603, "ignorable": true + }, + { + "message": "Method Levels\\AcceptTypes\\Implode::partlySupportedUnion() has no return type specified.", + "line": 755, + "ignorable": true + }, + { + "message": "Method Levels\\AcceptTypes\\Implode::partlySupportedUnion() has parameter $union with no value type specified in iterable type array.", + "line": 755, + "ignorable": true + }, + { + "message": "Method Levels\\AcceptTypes\\Implode::invalidType() has no return type specified.", + "line": 762, + "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/acceptTypes-7.json b/tests/PHPStan/Levels/data/acceptTypes-7.json index 76c6e1399e..8345aca34c 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-7.json +++ b/tests/PHPStan/Levels/data/acceptTypes-7.json @@ -35,7 +35,7 @@ "ignorable": true }, { - "message": "Parameter #1 $var of function count expects array|Countable, iterable given.", + "message": "Parameter #1 $value of function count expects array|Countable, iterable given.", "line": 167, "ignorable": true }, @@ -100,22 +100,22 @@ "ignorable": true }, { - "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array('foo' => callable(): mixed), array given.", + "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array given.", "line": 577, "ignorable": true }, { - "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array('foo' => callable(): mixed), array given.", + "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array given.", "line": 578, "ignorable": true }, { - "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array('foo' => callable(): mixed), array()|array('foo' => 'date') given.", + "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, array{}|array{foo: 'date'} given.", "line": 596, "ignorable": true }, { - "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array('foo' => callable(): mixed), iterable given.", + "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array{foo: callable(): mixed}, iterable given.", "line": 597, "ignorable": true }, @@ -153,5 +153,10 @@ "message": "Parameter #1 $min (int<-1, 1>) of function random_int expects lower number than parameter #2 $max (int<-1, 1>).", "line": 692, "ignorable": true + }, + { + "message": "Parameter #2 $array of function implode expects array|null, array|int|string given.", + "line": 756, + "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/acceptTypes.php b/tests/PHPStan/Levels/data/acceptTypes.php index bc99693645..d16184f062 100644 --- a/tests/PHPStan/Levels/data/acceptTypes.php +++ b/tests/PHPStan/Levels/data/acceptTypes.php @@ -747,3 +747,19 @@ public function doBar( } } + +class Implode { + /** + * @param string|int|array $union + */ + public function partlySupportedUnion($union) { + $imploded = implode('abc', $union); + } + + /** + * @param int $invalid + */ + public function invalidType($invalid) { + $imploded = implode('abc', $invalid); + } +} diff --git a/tests/PHPStan/Levels/data/arrayAccess-6.json b/tests/PHPStan/Levels/data/arrayAccess-6.json index bf65c24f00..3a8d649bcb 100644 --- a/tests/PHPStan/Levels/data/arrayAccess-6.json +++ b/tests/PHPStan/Levels/data/arrayAccess-6.json @@ -1,26 +1,26 @@ [ { - "message": "Method Levels\\ArrayAccess\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\ArrayAccess\\Foo::doFoo() has no return type specified.", "line": 11, "ignorable": true }, { - "message": "Method Levels\\ArrayAccess\\Foo::doBar() has no return typehint specified.", + "message": "Method Levels\\ArrayAccess\\Foo::doBar() has no return type specified.", "line": 22, "ignorable": true }, { - "message": "Method Levels\\ArrayAccess\\Foo::doBaz() has no return typehint specified.", + "message": "Method Levels\\ArrayAccess\\Foo::doBaz() has no return type specified.", "line": 30, "ignorable": true }, { - "message": "Method Levels\\ArrayAccess\\Foo::doLorem() has no return typehint specified.", + "message": "Method Levels\\ArrayAccess\\Foo::doLorem() has no return type specified.", "line": 38, "ignorable": true }, { - "message": "Method Levels\\ArrayAccess\\Foo::doLorem() has parameter $mixed with no typehint specified.", + "message": "Method Levels\\ArrayAccess\\Foo::doLorem() has parameter $mixed with no type specified.", "line": 38, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/arrayDestructuring-3.json b/tests/PHPStan/Levels/data/arrayDestructuring-3.json index 710133680d..a97c71f0f5 100644 --- a/tests/PHPStan/Levels/data/arrayDestructuring-3.json +++ b/tests/PHPStan/Levels/data/arrayDestructuring-3.json @@ -5,7 +5,7 @@ "ignorable": true }, { - "message": "Offset 3 does not exist on array('a', 'b', 'c').", + "message": "Offset 3 does not exist on array{'a', 'b', 'c'}.", "line": 30, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/arrayDestructuring.php b/tests/PHPStan/Levels/data/arrayDestructuring.php index 5d7e61ea93..c97a3e6328 100644 --- a/tests/PHPStan/Levels/data/arrayDestructuring.php +++ b/tests/PHPStan/Levels/data/arrayDestructuring.php @@ -1,6 +1,6 @@ 1).", + "message": "Offset 'b' does not exist on array{a: 1}.", "line": 21, "ignorable": true } -] +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/arrayDimFetches-6.json b/tests/PHPStan/Levels/data/arrayDimFetches-6.json index dfc3b07751..1a39a16f63 100644 --- a/tests/PHPStan/Levels/data/arrayDimFetches-6.json +++ b/tests/PHPStan/Levels/data/arrayDimFetches-6.json @@ -1,11 +1,11 @@ [ { - "message": "Method Levels\\ArrayDimFetches\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\ArrayDimFetches\\Foo::doFoo() has no return type specified.", "line": 12, "ignorable": true }, { - "message": "Method Levels\\ArrayDimFetches\\Foo::doBar() has no return typehint specified.", + "message": "Method Levels\\ArrayDimFetches\\Foo::doBar() has no return type specified.", "line": 31, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/arrayDimFetches-7.json b/tests/PHPStan/Levels/data/arrayDimFetches-7.json index 15e551e874..25e275b28e 100644 --- a/tests/PHPStan/Levels/data/arrayDimFetches-7.json +++ b/tests/PHPStan/Levels/data/arrayDimFetches-7.json @@ -5,17 +5,17 @@ "ignorable": true }, { - "message": "Cannot access offset 'a' on array('a' => 1)|stdClass.", + "message": "Cannot access offset 'a' on array{a: 1}|stdClass.", "line": 27, "ignorable": true }, { - "message": "Cannot access offset 'b' on array('a' => 1)|stdClass.", + "message": "Cannot access offset 'b' on array{a: 1}|stdClass.", "line": 28, "ignorable": true }, { - "message": "Offset 'b' does not exist on array('a' => 1, ?'b' => 1).", + "message": "Offset 'b' does not exist on array{a: 1, b?: 1}.", "line": 40, "ignorable": true }, @@ -29,4 +29,4 @@ "line": 58, "ignorable": true } -] +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/arrayDimFetches-9.json b/tests/PHPStan/Levels/data/arrayDimFetches-9.json new file mode 100644 index 0000000000..fb1695ce25 --- /dev/null +++ b/tests/PHPStan/Levels/data/arrayDimFetches-9.json @@ -0,0 +1,7 @@ +[ + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 15, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/binaryOps-6.json b/tests/PHPStan/Levels/data/binaryOps-6.json index 41ae5dc405..f71cae89e6 100644 --- a/tests/PHPStan/Levels/data/binaryOps-6.json +++ b/tests/PHPStan/Levels/data/binaryOps-6.json @@ -1,6 +1,6 @@ [ { - "message": "Method Levels\\BinaryOps\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\BinaryOps\\Foo::doFoo() has no return type specified.", "line": 14, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/callableCalls-6.json b/tests/PHPStan/Levels/data/callableCalls-6.json index 1ac782fa92..ad0d233ac4 100644 --- a/tests/PHPStan/Levels/data/callableCalls-6.json +++ b/tests/PHPStan/Levels/data/callableCalls-6.json @@ -1,11 +1,11 @@ [ { - "message": "Method Levels\\CallableCalls\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\CallableCalls\\Foo::doFoo() has no return type specified.", "line": 14, "ignorable": true }, { - "message": "Method Levels\\CallableCalls\\Foo::doBar() has no return typehint specified.", + "message": "Method Levels\\CallableCalls\\Foo::doBar() has no return type specified.", "line": 41, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/callableCalls-9-missing.json b/tests/PHPStan/Levels/data/callableCalls-9-missing.json new file mode 100644 index 0000000000..5c7f12b38d --- /dev/null +++ b/tests/PHPStan/Levels/data/callableCalls-9-missing.json @@ -0,0 +1,12 @@ +[ + { + "message": "Closure invoked with 0 parameters, 1 required.", + "line": 37, + "ignorable": true + }, + { + "message": "Trying to invoke int but it's not a callable.", + "line": 43, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/callableVariance-6.json b/tests/PHPStan/Levels/data/callableVariance-6.json index d5b814feae..28eec65b64 100644 --- a/tests/PHPStan/Levels/data/callableVariance-6.json +++ b/tests/PHPStan/Levels/data/callableVariance-6.json @@ -1,11 +1,11 @@ [ { - "message": "Function Levels\\CallableVariance\\d() has no return typehint specified.", + "message": "Function Levels\\CallableVariance\\d() has no return type specified.", "line": 68, "ignorable": true }, { - "message": "Function Levels\\CallableVariance\\testD() has no return typehint specified.", + "message": "Function Levels\\CallableVariance\\testD() has no return type specified.", "line": 79, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/casts-2.json b/tests/PHPStan/Levels/data/casts-2.json deleted file mode 100644 index 3fef9a1842..0000000000 --- a/tests/PHPStan/Levels/data/casts-2.json +++ /dev/null @@ -1,12 +0,0 @@ -[ - { - "message": "Cannot cast array to int.", - "line": 19, - "ignorable": true - }, - { - "message": "Cannot cast array|(callable(): mixed) to int.", - "line": 20, - "ignorable": true - } -] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/casts-6.json b/tests/PHPStan/Levels/data/casts-6.json index 842590fde6..1fc1590698 100644 --- a/tests/PHPStan/Levels/data/casts-6.json +++ b/tests/PHPStan/Levels/data/casts-6.json @@ -1,6 +1,6 @@ [ { - "message": "Method Levels\\Casts\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\Casts\\Foo::doFoo() has no return type specified.", "line": 13, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/casts-7.json b/tests/PHPStan/Levels/data/casts-7.json index 40dd8a8de5..d9b7a85b78 100644 --- a/tests/PHPStan/Levels/data/casts-7.json +++ b/tests/PHPStan/Levels/data/casts-7.json @@ -1,4 +1,9 @@ [ + { + "message": "Cannot cast array|(callable(): mixed) to int.", + "line": 20, + "ignorable": true + }, { "message": "Cannot cast array|float|int to string.", "line": 21, diff --git a/tests/PHPStan/Levels/data/clone-6.json b/tests/PHPStan/Levels/data/clone-6.json index 20f2c4389a..5a448fefc5 100644 --- a/tests/PHPStan/Levels/data/clone-6.json +++ b/tests/PHPStan/Levels/data/clone-6.json @@ -1,6 +1,6 @@ [ { - "message": "Method Levels\\Cloning\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\Cloning\\Foo::doFoo() has no return type specified.", "line": 18, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/clone-9-missing.json b/tests/PHPStan/Levels/data/clone-9-missing.json new file mode 100644 index 0000000000..40e1203120 --- /dev/null +++ b/tests/PHPStan/Levels/data/clone-9-missing.json @@ -0,0 +1,12 @@ +[ + { + "message": "Cannot clone non-object variable $nullableInt of type int.", + "line": 34, + "ignorable": true + }, + { + "message": "Cannot clone non-object variable $nullableUnion of type int|Levels\\Cloning\\Foo.", + "line": 35, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/clone-9.json b/tests/PHPStan/Levels/data/clone-9.json new file mode 100644 index 0000000000..8148322a42 --- /dev/null +++ b/tests/PHPStan/Levels/data/clone-9.json @@ -0,0 +1,7 @@ +[ + { + "message": "Cannot clone non-object variable $mixed of type mixed.", + "line": 36, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/coalesce-0.json b/tests/PHPStan/Levels/data/coalesce-0.json deleted file mode 100644 index d4d84323dd..0000000000 --- a/tests/PHPStan/Levels/data/coalesce-0.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "message": "Undefined variable: $bar", - "line": 6, - "ignorable": true - } -] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/coalesce-1.json b/tests/PHPStan/Levels/data/coalesce-1.json index 6636fbc672..f329d4e78d 100644 --- a/tests/PHPStan/Levels/data/coalesce-1.json +++ b/tests/PHPStan/Levels/data/coalesce-1.json @@ -8,5 +8,15 @@ "message": "Variable $bar on left side of ?? is never defined.", "line": 6, "ignorable": true + }, + { + "message": "Variable $a on left side of ?? is never defined.", + "line": 15, + "ignorable": true + }, + { + "message": "Variable $s on left side of ?? always exists and is always null.", + "line": 23, + "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/coalesce.php b/tests/PHPStan/Levels/data/coalesce.php index 6010637b0a..fffbf433ca 100644 --- a/tests/PHPStan/Levels/data/coalesce.php +++ b/tests/PHPStan/Levels/data/coalesce.php @@ -10,3 +10,15 @@ function (\ReflectionClass $ref): void { echo $ref->name ?? 'foo'; echo $ref->nonexistent ?? 'bar'; }; + +function (?string $s): void { + echo $a ?? 'foo'; + + echo $s ?? 'bar'; + + if ($s !== null) { + return; + } + + echo $s ?? 'bar'; +}; diff --git a/tests/PHPStan/Levels/data/comparison-6.json b/tests/PHPStan/Levels/data/comparison-6.json index 39a509dbc4..c470766a2e 100644 --- a/tests/PHPStan/Levels/data/comparison-6.json +++ b/tests/PHPStan/Levels/data/comparison-6.json @@ -1,6 +1,6 @@ [ { - "message": "Method Levels\\Comparison\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\Comparison\\Foo::doFoo() has no return type specified.", "line": 18, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/constantAccesses-6.json b/tests/PHPStan/Levels/data/constantAccesses-6.json index 8c3f1d74ee..2a9e2775c7 100644 --- a/tests/PHPStan/Levels/data/constantAccesses-6.json +++ b/tests/PHPStan/Levels/data/constantAccesses-6.json @@ -1,11 +1,11 @@ [ { - "message": "Method Levels\\ConstantAccesses\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\ConstantAccesses\\Foo::doFoo() has no return type specified.", "line": 14, "ignorable": true }, { - "message": "Method Levels\\ConstantAccesses\\Baz::doBaz() has no return typehint specified.", + "message": "Method Levels\\ConstantAccesses\\Baz::doBaz() has no return type specified.", "line": 42, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/constantAccesses-9-missing.json b/tests/PHPStan/Levels/data/constantAccesses-9-missing.json new file mode 100644 index 0000000000..0cc5a3f5d4 --- /dev/null +++ b/tests/PHPStan/Levels/data/constantAccesses-9-missing.json @@ -0,0 +1,17 @@ +[ + { + "message": "Access to undefined constant Levels\\ConstantAccesses\\Foo::BAR_CONSTANT.", + "line": 53, + "ignorable": true + }, + { + "message": "Access to undefined constant Levels\\ConstantAccesses\\Bar|Levels\\ConstantAccesses\\Foo::BAR_CONSTANT.", + "line": 56, + "ignorable": true + }, + { + "message": "Access to undefined constant Levels\\ConstantAccesses\\Bar|Levels\\ConstantAccesses\\Foo::FOO_CONSTANT.", + "line": 55, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/inferPropertyType-6.json b/tests/PHPStan/Levels/data/inferPropertyType-6.json index 20a2ad3c6e..42ce35fafd 100644 --- a/tests/PHPStan/Levels/data/inferPropertyType-6.json +++ b/tests/PHPStan/Levels/data/inferPropertyType-6.json @@ -1,11 +1,11 @@ [ { - "message": "Property InferPropertyType\\Foo::$bar has no typehint specified.", + "message": "Property InferPropertyType\\Foo::$bar has no type specified.", "line": 10, "ignorable": true }, { - "message": "Method InferPropertyType\\Foo::doFoo() has no return typehint specified.", + "message": "Method InferPropertyType\\Foo::doFoo() has no return type specified.", "line": 18, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/iterable-6.json b/tests/PHPStan/Levels/data/iterable-6.json index d2cccc8bcb..d77f788ce1 100644 --- a/tests/PHPStan/Levels/data/iterable-6.json +++ b/tests/PHPStan/Levels/data/iterable-6.json @@ -1,6 +1,6 @@ [ { - "message": "Method Levels\\Iterables\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\Iterables\\Foo::doFoo() has no return type specified.", "line": 15, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/methodCalls-6.json b/tests/PHPStan/Levels/data/methodCalls-6.json index d1f740f5cb..60f6147b69 100644 --- a/tests/PHPStan/Levels/data/methodCalls-6.json +++ b/tests/PHPStan/Levels/data/methodCalls-6.json @@ -1,71 +1,71 @@ [ { - "message": "Method Levels\\MethodCalls\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\MethodCalls\\Foo::doFoo() has no return type specified.", "line": 8, "ignorable": true }, { - "message": "Method Levels\\MethodCalls\\Bar::doBar() has no return typehint specified.", + "message": "Method Levels\\MethodCalls\\Bar::doBar() has no return type specified.", "line": 23, "ignorable": true }, { - "message": "Method Levels\\MethodCalls\\Baz::doBaz() has no return typehint specified.", + "message": "Method Levels\\MethodCalls\\Baz::doBaz() has no return type specified.", "line": 45, "ignorable": true }, { - "message": "Method Levels\\MethodCalls\\ClassWithMagicMethod::doFoo() has no return typehint specified.", + "message": "Method Levels\\MethodCalls\\ClassWithMagicMethod::doFoo() has no return type specified.", "line": 70, "ignorable": true }, { - "message": "Method Levels\\MethodCalls\\ClassWithMagicMethod::__call() has no return typehint specified.", + "message": "Method Levels\\MethodCalls\\ClassWithMagicMethod::__call() has no return type specified.", "line": 79, "ignorable": true }, { - "message": "Method Levels\\MethodCalls\\AnotherClassWithMagicMethod::doFoo() has no return typehint specified.", + "message": "Method Levels\\MethodCalls\\AnotherClassWithMagicMethod::doFoo() has no return type specified.", "line": 89, "ignorable": true }, { - "message": "Method Levels\\MethodCalls\\AnotherClassWithMagicMethod::__callStatic() has no return typehint specified.", + "message": "Method Levels\\MethodCalls\\AnotherClassWithMagicMethod::__callStatic() has no return type specified.", "line": 98, "ignorable": true }, { - "message": "Method Levels\\MethodCalls\\Ipsum::doLorem() has no return typehint specified.", + "message": "Method Levels\\MethodCalls\\Ipsum::doLorem() has no return type specified.", "line": 158, "ignorable": true }, { - "message": "Method Levels\\MethodCalls\\FooException::commonMethod() has no return typehint specified.", + "message": "Method Levels\\MethodCalls\\FooException::commonMethod() has no return type specified.", "line": 182, "ignorable": true }, { - "message": "Method Levels\\MethodCalls\\FooException::doFoo() has no return typehint specified.", + "message": "Method Levels\\MethodCalls\\FooException::doFoo() has no return type specified.", "line": 187, "ignorable": true }, { - "message": "Method Levels\\MethodCalls\\BarException::commonMethod() has no return typehint specified.", + "message": "Method Levels\\MethodCalls\\BarException::commonMethod() has no return type specified.", "line": 197, "ignorable": true }, { - "message": "Method Levels\\MethodCalls\\BarException::doBar() has no return typehint specified.", + "message": "Method Levels\\MethodCalls\\BarException::doBar() has no return type specified.", "line": 202, "ignorable": true }, { - "message": "Method Levels\\MethodCalls\\TestExceptions::doFoo() has no return typehint specified.", + "message": "Method Levels\\MethodCalls\\TestExceptions::doFoo() has no return type specified.", "line": 212, "ignorable": true }, { - "message": "Method Levels\\MethodCalls\\ExtraArguments::doFoo() has no return typehint specified.", + "message": "Method Levels\\MethodCalls\\ExtraArguments::doFoo() has no return type specified.", "line": 234, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/methodCalls-9-missing.json b/tests/PHPStan/Levels/data/methodCalls-9-missing.json new file mode 100644 index 0000000000..47cdcab769 --- /dev/null +++ b/tests/PHPStan/Levels/data/methodCalls-9-missing.json @@ -0,0 +1,52 @@ +[ + { + "message": "Method Levels\\MethodCalls\\Foo::doFoo() invoked with 0 parameters, 1 required.", + "line": 53, + "ignorable": true + }, + { + "message": "Method Levels\\MethodCalls\\Foo::doFoo() invoked with 0 parameters, 1 required.", + "line": 56, + "ignorable": true + }, + { + "message": "Method Levels\\MethodCalls\\Foo::doFoo() invoked with 0 parameters, 1 required.", + "line": 59, + "ignorable": true + }, + { + "message": "Method Levels\\MethodCalls\\Foo::doFoo() invoked with 0 parameters, 1 required.", + "line": 162, + "ignorable": true + }, + { + "message": "Method Levels\\MethodCalls\\Foo::doFoo() invoked with 0 parameters, 1 required.", + "line": 166, + "ignorable": true + }, + { + "message": "Method Levels\\MethodCalls\\Foo::doFoo() invoked with 0 parameters, 1 required.", + "line": 170, + "ignorable": true + }, + { + "message": "Call to an undefined method Levels\\MethodCalls\\Bar|Levels\\MethodCalls\\Foo::doFoo().", + "line": 59, + "ignorable": true + }, + { + "message": "Call to an undefined method Levels\\MethodCalls\\Bar|Levels\\MethodCalls\\Foo::doFoo().", + "line": 60, + "ignorable": true + }, + { + "message": "Call to an undefined method Levels\\MethodCalls\\Bar|Levels\\MethodCalls\\Foo::doFoo().", + "line": 170, + "ignorable": true + }, + { + "message": "Call to an undefined method Levels\\MethodCalls\\Bar|Levels\\MethodCalls\\Foo::doFoo().", + "line": 171, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/missingReturn-0.json b/tests/PHPStan/Levels/data/missingReturn-0.json index d11943ca76..98754e893f 100644 --- a/tests/PHPStan/Levels/data/missingReturn-0.json +++ b/tests/PHPStan/Levels/data/missingReturn-0.json @@ -2,6 +2,11 @@ { "message": "Method Levels\\MissingReturn\\Foo::doFoo() should return int but return statement is missing.", "line": 8, + "ignorable": false + }, + { + "message": "Method Levels\\MissingReturn\\Foo::doBar() should return int but return statement is missing.", + "line": 16, "ignorable": true } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/missingReturn-2.json b/tests/PHPStan/Levels/data/missingReturn-2.json deleted file mode 100644 index 3a4788f45d..0000000000 --- a/tests/PHPStan/Levels/data/missingReturn-2.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "message": "Method Levels\\MissingReturn\\Foo::doBar() should return int but return statement is missing.", - "line": 16, - "ignorable": true - } -] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/object-6.json b/tests/PHPStan/Levels/data/object-6.json index f98cc39107..3110d6eed2 100644 --- a/tests/PHPStan/Levels/data/object-6.json +++ b/tests/PHPStan/Levels/data/object-6.json @@ -1,16 +1,16 @@ [ { - "message": "Method Levels\\ObjectTests\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\ObjectTests\\Foo::doFoo() has no return type specified.", "line": 11, "ignorable": true }, { - "message": "Method Levels\\ObjectTests\\Foo::doBar() has no return typehint specified.", + "message": "Method Levels\\ObjectTests\\Foo::doBar() has no return type specified.", "line": 23, "ignorable": true }, { - "message": "Method Levels\\ObjectTests\\Foo::doBaz() has no return typehint specified.", + "message": "Method Levels\\ObjectTests\\Foo::doBaz() has no return type specified.", "line": 35, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/object-9-missing.json b/tests/PHPStan/Levels/data/object-9-missing.json new file mode 100644 index 0000000000..4d1f2153ba --- /dev/null +++ b/tests/PHPStan/Levels/data/object-9-missing.json @@ -0,0 +1,22 @@ +[ + { + "message": "Call to an undefined method object::foo().", + "line": 25, + "ignorable": true + }, + { + "message": "Access to an undefined property object::$bar.", + "line": 26, + "ignorable": true + }, + { + "message": "Call to an undefined static method object::baz().", + "line": 28, + "ignorable": true + }, + { + "message": "Access to an undefined static property object::$dolor.", + "line": 29, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/propertyAccesses-6.json b/tests/PHPStan/Levels/data/propertyAccesses-6.json index f0d47cd16e..edeb6d1b42 100644 --- a/tests/PHPStan/Levels/data/propertyAccesses-6.json +++ b/tests/PHPStan/Levels/data/propertyAccesses-6.json @@ -1,41 +1,41 @@ [ { - "message": "Method Levels\\PropertyAccesses\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\PropertyAccesses\\Foo::doFoo() has no return type specified.", "line": 11, "ignorable": true }, { - "message": "Method Levels\\PropertyAccesses\\Bar::doBar() has no return typehint specified.", + "message": "Method Levels\\PropertyAccesses\\Bar::doBar() has no return type specified.", "line": 29, "ignorable": true }, { - "message": "Method Levels\\PropertyAccesses\\Baz::doBaz() has no return typehint specified.", + "message": "Method Levels\\PropertyAccesses\\Baz::doBaz() has no return type specified.", "line": 50, "ignorable": true }, { - "message": "Method Levels\\PropertyAccesses\\ClassWithMagicMethod::doFoo() has no return typehint specified.", + "message": "Method Levels\\PropertyAccesses\\ClassWithMagicMethod::doFoo() has no return type specified.", "line": 74, "ignorable": true }, { - "message": "Method Levels\\PropertyAccesses\\ClassWithMagicMethod::__set() has no return typehint specified.", + "message": "Method Levels\\PropertyAccesses\\ClassWithMagicMethod::__set() has no return type specified.", "line": 83, "ignorable": true }, { - "message": "Method Levels\\PropertyAccesses\\AnotherClassWithMagicMethod::doFoo() has no return typehint specified.", + "message": "Method Levels\\PropertyAccesses\\AnotherClassWithMagicMethod::doFoo() has no return type specified.", "line": 93, "ignorable": true }, { - "message": "Method Levels\\PropertyAccesses\\AnotherClassWithMagicMethod::__get() has no return typehint specified.", + "message": "Method Levels\\PropertyAccesses\\AnotherClassWithMagicMethod::__get() has no return type specified.", "line": 98, "ignorable": true }, { - "message": "Method Levels\\PropertyAccesses\\Ipsum::doBaz() has no return typehint specified.", + "message": "Method Levels\\PropertyAccesses\\Ipsum::doBaz() has no return type specified.", "line": 158, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/propertyAccesses-9-missing.json b/tests/PHPStan/Levels/data/propertyAccesses-9-missing.json new file mode 100644 index 0000000000..1a8bc8b4b7 --- /dev/null +++ b/tests/PHPStan/Levels/data/propertyAccesses-9-missing.json @@ -0,0 +1,32 @@ +[ + { + "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", + "line": 61, + "ignorable": true + }, + { + "message": "Access to an undefined property Levels\\PropertyAccesses\\Foo::$bar.", + "line": 166, + "ignorable": true + }, + { + "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$foo.", + "line": 63, + "ignorable": true + }, + { + "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$bar.", + "line": 64, + "ignorable": true + }, + { + "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$foo.", + "line": 169, + "ignorable": true + }, + { + "message": "Access to an undefined property Levels\\PropertyAccesses\\Bar|Levels\\PropertyAccesses\\Foo::$bar.", + "line": 170, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/returnTypes-2.json b/tests/PHPStan/Levels/data/returnTypes-0.json similarity index 100% rename from tests/PHPStan/Levels/data/returnTypes-2.json rename to tests/PHPStan/Levels/data/returnTypes-0.json diff --git a/tests/PHPStan/Levels/data/stringOffsetAccess-9.json b/tests/PHPStan/Levels/data/stringOffsetAccess-9.json new file mode 100644 index 0000000000..1e218ab052 --- /dev/null +++ b/tests/PHPStan/Levels/data/stringOffsetAccess-9.json @@ -0,0 +1,42 @@ +[ + { + "message": "Cannot access offset 0 on mixed.", + "line": 39, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 39, + "ignorable": true + }, + { + "message": "Cannot access offset 'foo' on mixed.", + "line": 43, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 43, + "ignorable": true + }, + { + "message": "Cannot access offset 12.34 on mixed.", + "line": 47, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 47, + "ignorable": true + }, + { + "message": "Cannot access offset int|object on mixed.", + "line": 51, + "ignorable": true + }, + { + "message": "Parameter #1 (mixed) of echo cannot be converted to string.", + "line": 51, + "ignorable": true + } +] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/stubValidator-0.json b/tests/PHPStan/Levels/data/stubValidator-0.json index 0625085298..0517d298af 100644 --- a/tests/PHPStan/Levels/data/stubValidator-0.json +++ b/tests/PHPStan/Levels/data/stubValidator-0.json @@ -1,32 +1,32 @@ [ { - "message": "Method StubValidator\\Foo::doFoo() has no return typehint specified.", + "message": "Method StubValidator\\Foo::doFoo() has no return type specified.", "line": 15, - "ignorable": true + "ignorable": false }, { "message": "Method StubValidator\\Foo::doFoo() has parameter $argument with no value type specified in iterable type array.", "line": 15, - "ignorable": true + "ignorable": false }, { - "message": "Function StubValidator\\someFunction() has no return typehint specified.", + "message": "Function StubValidator\\someFunction() has no return type specified.", "line": 22, - "ignorable": true + "ignorable": false }, { "message": "Function StubValidator\\someFunction() has parameter $argument with no value type specified in iterable type array.", "line": 22, - "ignorable": true + "ignorable": false }, { - "message": "Method class@anonymous/stubValidator/stubs.php:27::doFoo() has no return typehint specified.", + "message": "Method class@anonymous/stubValidator/stubs.php:27::doFoo() has no return type specified.", "line": 30, - "ignorable": true + "ignorable": false }, { - "message": "Parameter $foo of method class@anonymous/stubValidator/stubs.php:27::doFoo() has invalid typehint type StubValidator\\Foooooooo.", + "message": "Parameter $foo of method class@anonymous/stubValidator/stubs.php:27::doFoo() has invalid type StubValidator\\Foooooooo.", "line": 30, - "ignorable": true + "ignorable": false } -] \ No newline at end of file +] diff --git a/tests/PHPStan/Levels/data/stubs-functions-6.json b/tests/PHPStan/Levels/data/stubs-functions-6.json index 2b701606cf..68b139c505 100644 --- a/tests/PHPStan/Levels/data/stubs-functions-6.json +++ b/tests/PHPStan/Levels/data/stubs-functions-6.json @@ -1,11 +1,11 @@ [ { - "message": "Function StubsIntegrationTest\\foo() has no return typehint specified.", + "message": "Function StubsIntegrationTest\\foo() has no return type specified.", "line": 5, "ignorable": true }, { - "message": "Function StubsIntegrationTest\\foo() has parameter $i with no typehint specified.", + "message": "Function StubsIntegrationTest\\foo() has parameter $i with no type specified.", "line": 5, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/stubs-methods-3.json b/tests/PHPStan/Levels/data/stubs-methods-3.json index 1c0402a627..9f8132c6e9 100644 --- a/tests/PHPStan/Levels/data/stubs-methods-3.json +++ b/tests/PHPStan/Levels/data/stubs-methods-3.json @@ -23,10 +23,5 @@ "message": "Anonymous function should return int but returns string.", "line": 128, "ignorable": true - }, - { - "message": "Method StubsIntegrationTest\\YetYetAnotherFoo::doFoo() should return stdClass but returns string.", - "line": 219, - "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/stubs-methods-4.json b/tests/PHPStan/Levels/data/stubs-methods-4.json index ab8a491ae3..eb0d8a3325 100644 --- a/tests/PHPStan/Levels/data/stubs-methods-4.json +++ b/tests/PHPStan/Levels/data/stubs-methods-4.json @@ -1,36 +1,36 @@ [ { - "message": "Strict comparison using === between int and array() will always evaluate to false.", + "message": "Strict comparison using === between int and array{} will always evaluate to false.", "line": 47, "ignorable": true }, { - "message": "Strict comparison using === between int and array() will always evaluate to false.", + "message": "Strict comparison using === between int and array{} will always evaluate to false.", "line": 58, "ignorable": true }, { - "message": "Strict comparison using === between int and array() will always evaluate to false.", + "message": "Strict comparison using === between int and array{} will always evaluate to false.", "line": 89, "ignorable": true }, { - "message": "Strict comparison using === between int and array() will always evaluate to false.", + "message": "Strict comparison using === between int and array{} will always evaluate to false.", "line": 108, "ignorable": true }, { - "message": "Strict comparison using === between int and array() will always evaluate to false.", + "message": "Strict comparison using === between int and array{} will always evaluate to false.", "line": 144, "ignorable": true }, { - "message": "Strict comparison using === between int and array() will always evaluate to false.", + "message": "Strict comparison using === between int and array{} will always evaluate to false.", "line": 158, "ignorable": true }, { - "message": "Strict comparison using === between int and array() will always evaluate to false.", + "message": "Strict comparison using === between int and array{} will always evaluate to false.", "line": 175, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/stubs-methods-6.json b/tests/PHPStan/Levels/data/stubs-methods-6.json deleted file mode 100644 index 9b15e1a786..0000000000 --- a/tests/PHPStan/Levels/data/stubs-methods-6.json +++ /dev/null @@ -1,27 +0,0 @@ -[ - { - "message": "Method StubsIntegrationTest\\Foo::doFoo() has no return typehint specified.", - "line": 8, - "ignorable": true - }, - { - "message": "Method StubsIntegrationTest\\Foo::doFoo() has parameter $i with no typehint specified.", - "line": 8, - "ignorable": true - }, - { - "message": "Method StubsIntegrationTest\\InterfaceWithStubPhpDoc2::doFoo() has no return typehint specified.", - "line": 151, - "ignorable": true - }, - { - "message": "Method StubsIntegrationTest\\YetAnotherFoo::doFoo() has no return typehint specified.", - "line": 197, - "ignorable": true - }, - { - "message": "Method StubsIntegrationTest\\YetAnotherFoo::doFoo() has parameter $j with no typehint specified.", - "line": 197, - "ignorable": true - } -] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/throwValues-6.json b/tests/PHPStan/Levels/data/throwValues-6.json index ae6f775eef..ccbd412f3b 100644 --- a/tests/PHPStan/Levels/data/throwValues-6.json +++ b/tests/PHPStan/Levels/data/throwValues-6.json @@ -1,6 +1,6 @@ [ { - "message": "Method Levels\\ThrowValues\\Foo::doFoo() has no return typehint specified.", + "message": "Method Levels\\ThrowValues\\Foo::doFoo() has no return type specified.", "line": 30, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/typehints-0.json b/tests/PHPStan/Levels/data/typehints-0.json index 07084c7e86..46421c021f 100644 --- a/tests/PHPStan/Levels/data/typehints-0.json +++ b/tests/PHPStan/Levels/data/typehints-0.json @@ -1,11 +1,11 @@ [ { - "message": "Parameter $lorem of method Levels\\Typehints\\Foo::doFoo() has invalid typehint type Levels\\Typehints\\Lorem.", + "message": "Method Levels\\Typehints\\Foo::doFoo() has invalid return type Levels\\Typehints\\Ipsum.", "line": 8, "ignorable": true }, { - "message": "Return typehint of method Levels\\Typehints\\Foo::doFoo() has invalid type Levels\\Typehints\\Ipsum.", + "message": "Parameter $lorem of method Levels\\Typehints\\Foo::doFoo() has invalid type Levels\\Typehints\\Lorem.", "line": 8, "ignorable": true }, diff --git a/tests/PHPStan/Levels/data/typehints-2.json b/tests/PHPStan/Levels/data/typehints-2.json index adbf371a07..1f1a947306 100644 --- a/tests/PHPStan/Levels/data/typehints-2.json +++ b/tests/PHPStan/Levels/data/typehints-2.json @@ -1,11 +1,11 @@ [ { - "message": "Parameter $lorem of method Levels\\Typehints\\Foo::doBar() has invalid typehint type Levels\\Typehints\\Lorem.", + "message": "Method Levels\\Typehints\\Foo::doBar() has invalid return type Levels\\Typehints\\Ipsum.", "line": 17, "ignorable": true }, { - "message": "Return typehint of method Levels\\Typehints\\Foo::doBar() has invalid type Levels\\Typehints\\Ipsum.", + "message": "Parameter $lorem of method Levels\\Typehints\\Foo::doBar() has invalid type Levels\\Typehints\\Lorem.", "line": 17, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/unreachable-6-alwaysTrue.json b/tests/PHPStan/Levels/data/unreachable-6-alwaysTrue.json index 05edefa8b4..b285fc696d 100644 --- a/tests/PHPStan/Levels/data/unreachable-6-alwaysTrue.json +++ b/tests/PHPStan/Levels/data/unreachable-6-alwaysTrue.json @@ -1,61 +1,61 @@ [ { - "message": "Method Levels\\Unreachable\\Foo::doStrictComparison() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Foo::doStrictComparison() has no return type specified.", "line": 8, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Foo::doInstanceOf() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Foo::doInstanceOf() has no return type specified.", "line": 18, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Foo::doTypeSpecifyingFunction() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Foo::doTypeSpecifyingFunction() has no return type specified.", "line": 27, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Foo::doOtherFunction() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Foo::doOtherFunction() has no return type specified.", "line": 36, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Foo::doOtherValue() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Foo::doOtherValue() has no return type specified.", "line": 45, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Foo::doBooleanAnd() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Foo::doBooleanAnd() has no return type specified.", "line": 54, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Bar::doStrictComparison() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Bar::doStrictComparison() has no return type specified.", "line": 71, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Bar::doInstanceOf() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Bar::doInstanceOf() has no return type specified.", "line": 77, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Bar::doTypeSpecifyingFunction() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Bar::doTypeSpecifyingFunction() has no return type specified.", "line": 82, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Bar::doOtherFunction() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Bar::doOtherFunction() has no return type specified.", "line": 87, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Bar::doOtherValue() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Bar::doOtherValue() has no return type specified.", "line": 92, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Bar::doBooleanAnd() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Bar::doBooleanAnd() has no return type specified.", "line": 97, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/unreachable-6.json b/tests/PHPStan/Levels/data/unreachable-6.json index 05edefa8b4..b285fc696d 100644 --- a/tests/PHPStan/Levels/data/unreachable-6.json +++ b/tests/PHPStan/Levels/data/unreachable-6.json @@ -1,61 +1,61 @@ [ { - "message": "Method Levels\\Unreachable\\Foo::doStrictComparison() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Foo::doStrictComparison() has no return type specified.", "line": 8, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Foo::doInstanceOf() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Foo::doInstanceOf() has no return type specified.", "line": 18, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Foo::doTypeSpecifyingFunction() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Foo::doTypeSpecifyingFunction() has no return type specified.", "line": 27, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Foo::doOtherFunction() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Foo::doOtherFunction() has no return type specified.", "line": 36, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Foo::doOtherValue() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Foo::doOtherValue() has no return type specified.", "line": 45, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Foo::doBooleanAnd() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Foo::doBooleanAnd() has no return type specified.", "line": 54, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Bar::doStrictComparison() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Bar::doStrictComparison() has no return type specified.", "line": 71, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Bar::doInstanceOf() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Bar::doInstanceOf() has no return type specified.", "line": 77, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Bar::doTypeSpecifyingFunction() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Bar::doTypeSpecifyingFunction() has no return type specified.", "line": 82, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Bar::doOtherFunction() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Bar::doOtherFunction() has no return type specified.", "line": 87, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Bar::doOtherValue() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Bar::doOtherValue() has no return type specified.", "line": 92, "ignorable": true }, { - "message": "Method Levels\\Unreachable\\Bar::doBooleanAnd() has no return typehint specified.", + "message": "Method Levels\\Unreachable\\Bar::doBooleanAnd() has no return type specified.", "line": 97, "ignorable": true } diff --git a/tests/PHPStan/Node/FileNodeTest.php b/tests/PHPStan/Node/FileNodeTest.php index 07faa2dc3e..852ec3b136 100644 --- a/tests/PHPStan/Node/FileNodeTest.php +++ b/tests/PHPStan/Node/FileNodeTest.php @@ -6,8 +6,12 @@ use PHPStan\Analyser\Scope; use PHPStan\File\SimpleRelativePathHelper; use PHPStan\Rules\Rule; +use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Testing\RuleTestCase; +use function get_class; +use function sprintf; +use const DIRECTORY_SEPARATOR; class FileNodeTest extends RuleTestCase { @@ -22,9 +26,8 @@ public function getNodeType(): string } /** - * @param \PHPStan\Node\FileNode $node - * @param \PHPStan\Analyser\Scope $scope - * @return \PHPStan\Rules\RuleError[] + * @param FileNode $node + * @return RuleError[] */ public function processNode(Node $node, Scope $scope): array { @@ -38,7 +41,7 @@ public function processNode(Node $node, Scope $scope): array return [ RuleErrorBuilder::message( - sprintf('First node in file %s is: %s', $pathHelper->getRelativePath($scope->getFile()), get_class($nodes[0])) + sprintf('First node in file %s is: %s', $pathHelper->getRelativePath($scope->getFile()), get_class($nodes[0])), )->build(), ]; } @@ -69,9 +72,6 @@ public function dataRule(): iterable /** * @dataProvider dataRule - * @param string $file - * @param string $expectedError - * @param int $line */ public function testRule(string $file, string $expectedError, int $line): void { diff --git a/tests/PHPStan/Parallel/ParallelAnalyserIntegrationTest.php b/tests/PHPStan/Parallel/ParallelAnalyserIntegrationTest.php index d438f44f48..5d5b136063 100644 --- a/tests/PHPStan/Parallel/ParallelAnalyserIntegrationTest.php +++ b/tests/PHPStan/Parallel/ParallelAnalyserIntegrationTest.php @@ -4,7 +4,14 @@ use Nette\Utils\Json; use PHPStan\File\FileHelper; +use PHPStan\ShouldNotHappenException; use PHPUnit\Framework\TestCase; +use function array_map; +use function escapeshellarg; +use function exec; +use function implode; +use function sprintf; +use const PHP_BINARY; /** * @group exec @@ -22,13 +29,12 @@ public function dataRun(): array /** * @dataProvider dataRun - * @param string $command */ public function testRun(string $command): void { exec(sprintf('%s %s clear-result-cache --configuration %s -q', escapeshellarg(PHP_BINARY), escapeshellarg(__DIR__ . '/../../../bin/phpstan'), escapeshellarg(__DIR__ . '/parallel-analyser.neon')), $clearResultCacheOutputLines, $clearResultCacheExitCode); if ($clearResultCacheExitCode !== 0) { - throw new \PHPStan\ShouldNotHappenException('Could not clear result cache.'); + throw new ShouldNotHappenException('Could not clear result cache.'); } exec(sprintf( @@ -37,12 +43,10 @@ public function testRun(string $command): void escapeshellarg(__DIR__ . '/../../../bin/phpstan'), $command, escapeshellarg(__DIR__ . '/parallel-analyser.neon'), - implode(' ', array_map(static function (string $path): string { - return escapeshellarg($path); - }, [ + implode(' ', array_map(static fn (string $path): string => escapeshellarg($path), [ __DIR__ . '/data/trait-definition.php', __DIR__ . '/data/traits.php', - ])) + ])), ), $outputLines, $exitCode); $output = implode("\n", $outputLines); @@ -51,24 +55,24 @@ public function testRun(string $command): void $this->assertJsonStringEqualsJsonString(Json::encode([ 'totals' => [ 'errors' => 0, - 'file_errors' => 3, + 'file_errors' => 4, ], 'files' => [ sprintf('%s (in context of class ParallelAnalyserIntegrationTest\\Bar)', $filePath) => [ 'errors' => 1, 'messages' => [ [ - 'message' => 'Method ParallelAnalyserIntegrationTest\\Bar::doFoo() has no return typehint specified.', + 'message' => 'Method ParallelAnalyserIntegrationTest\\Bar::doFoo() has no return type specified.', 'line' => 8, 'ignorable' => true, ], ], ], sprintf('%s (in context of class ParallelAnalyserIntegrationTest\\Foo)', $filePath) => [ - 'errors' => 2, + 'errors' => 3, 'messages' => [ [ - 'message' => 'Method ParallelAnalyserIntegrationTest\\Foo::doFoo() has no return typehint specified.', + 'message' => 'Method ParallelAnalyserIntegrationTest\\Foo::doFoo() has no return type specified.', 'line' => 8, 'ignorable' => true, ], @@ -77,6 +81,11 @@ public function testRun(string $command): void 'line' => 10, 'ignorable' => true, ], + [ + 'message' => 'Access to an undefined property ParallelAnalyserIntegrationTest\\Foo::$test.', + 'line' => 15, + 'ignorable' => true, + ], ], ], ], diff --git a/tests/PHPStan/Parallel/SchedulerTest.php b/tests/PHPStan/Parallel/SchedulerTest.php index e029e06d9b..0d63fc1380 100644 --- a/tests/PHPStan/Parallel/SchedulerTest.php +++ b/tests/PHPStan/Parallel/SchedulerTest.php @@ -3,6 +3,9 @@ namespace PHPStan\Parallel; use PHPUnit\Framework\TestCase; +use function array_fill; +use function array_map; +use function count; class SchedulerTest extends TestCase { @@ -69,12 +72,8 @@ public function dataSchedule(): array /** * @dataProvider dataSchedule - * @param int $cpuCores - * @param int $maximumNumberOfProcesses - * @param int $minimumNumberOfJobsPerProcess - * @param int $jobSize - * @param int $numberOfFiles - * @param int $expectedNumberOfProcesses + * @param positive-int $jobSize + * @param 0|positive-int $numberOfFiles * @param array $expectedJobSizes */ public function testSchedule( @@ -84,7 +83,7 @@ public function testSchedule( int $jobSize, int $numberOfFiles, int $expectedNumberOfProcesses, - array $expectedJobSizes + array $expectedJobSizes, ): void { $files = array_fill(0, $numberOfFiles, 'file.php'); @@ -92,9 +91,7 @@ public function testSchedule( $schedule = $scheduler->scheduleWork($cpuCores, $files); $this->assertSame($expectedNumberOfProcesses, $schedule->getNumberOfProcesses()); - $jobSizes = array_map(static function (array $job): int { - return count($job); - }, $schedule->getJobs()); + $jobSizes = array_map(static fn (array $job): int => count($job), $schedule->getJobs()); $this->assertSame($expectedJobSizes, $jobSizes); } diff --git a/tests/PHPStan/Parallel/data/trait-definition.php b/tests/PHPStan/Parallel/data/trait-definition.php index edf01e73f8..499e4040f7 100644 --- a/tests/PHPStan/Parallel/data/trait-definition.php +++ b/tests/PHPStan/Parallel/data/trait-definition.php @@ -10,4 +10,9 @@ public function doFoo() $this->test = 1; } + public function getFoo(): int + { + return $this->test; + } + } diff --git a/tests/PHPStan/Parser/CachedParserTest.php b/tests/PHPStan/Parser/CachedParserTest.php index a76dd3f97c..7d8323b458 100644 --- a/tests/PHPStan/Parser/CachedParserTest.php +++ b/tests/PHPStan/Parser/CachedParserTest.php @@ -2,11 +2,14 @@ namespace PHPStan\Parser; +use PhpParser\Node; + use PhpParser\Node\Stmt\Namespace_; +use PHPStan\File\FileHelper; use PHPStan\File\FileReader; -use PHPStan\Testing\TestCase; +use PHPStan\Testing\PHPStanTestCase; -class CachedParserTest extends TestCase +class CachedParserTest extends PHPStanTestCase { /** @@ -81,8 +84,16 @@ private function getPhpParserNodeMock(): \PhpParser\Node public function testParseTheSameFileWithDifferentMethod(): void { - $parser = new CachedParser(self::getContainer()->getService('pathRoutingParser'), 500); - $path = __DIR__ . '/data/test.php'; + $fileHelper = self::getContainer()->getByType(FileHelper::class); + $pathRoutingParser = new PathRoutingParser( + $fileHelper, + self::getContainer()->getService('currentPhpVersionRichParser'), + self::getContainer()->getService('currentPhpVersionSimpleDirectParser'), + self::getContainer()->getService('php8Parser') + ); + $parser = new CachedParser($pathRoutingParser, 500); + $path = $fileHelper->normalizePath(__DIR__ . '/data/test.php'); + $pathRoutingParser->setAnalysedFiles([$path]); $contents = FileReader::read($path); $stmts = $parser->parseString($contents); $this->assertInstanceOf(Namespace_::class, $stmts[0]); diff --git a/tests/PHPStan/Parser/CleaningParserTest.php b/tests/PHPStan/Parser/CleaningParserTest.php new file mode 100644 index 0000000000..7576eb828e --- /dev/null +++ b/tests/PHPStan/Parser/CleaningParserTest.php @@ -0,0 +1,79 @@ +parseFile($beforeFile); + $this->assertSame(FileReader::read($afterFile), "prettyPrint($ast) . "\n"); + } + +} diff --git a/tests/PHPStan/Parser/data/cleaning-1-after.php b/tests/PHPStan/Parser/data/cleaning-1-after.php new file mode 100644 index 0000000000..e13a195e7d --- /dev/null +++ b/tests/PHPStan/Parser/data/cleaning-1-after.php @@ -0,0 +1,41 @@ += 80100) { + doFoo1(); + doFoo2(); +} else { + doBar1(); + doBar2(); +} diff --git a/tests/PHPStan/Parser/data/cleaning-php-version-before2.php b/tests/PHPStan/Parser/data/cleaning-php-version-before2.php new file mode 100644 index 0000000000..4060f97c45 --- /dev/null +++ b/tests/PHPStan/Parser/data/cleaning-php-version-before2.php @@ -0,0 +1,13 @@ +', new IntersectionType([new ArrayType(new IntegerType(), new StringType()), new NonEmptyArrayType()])]; + yield ['class-string&literal-string', new IntersectionType([new ClassStringType(), new AccessoryLiteralStringType()])]; + yield ['class-string&literal-string', new IntersectionType([new GenericClassStringType(new ObjectType('Foo')), new AccessoryLiteralStringType()])]; + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(new ConstantStringType('foo'), new IntegerType()); + yield ['array{foo: int}', $builder->getArray()]; + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(new ConstantStringType('foo'), new IntegerType(), true); + yield ['array{foo?: int}', $builder->getArray()]; + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(new ConstantStringType('foo'), new IntegerType(), true); + $builder->setOffsetValueType(new ConstantStringType('bar'), new StringType()); + yield ['array{foo?: int, bar: string}', $builder->getArray()]; + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(null, new IntegerType()); + $builder->setOffsetValueType(null, new StringType()); + yield ['array{int, string}', $builder->getArray()]; + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(null, new IntegerType()); + $builder->setOffsetValueType(null, new StringType(), true); + yield ['array{0: int, 1?: string}', $builder->getArray()]; + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(new ConstantStringType('\'foo\''), new IntegerType()); + yield ['array{"\'foo\'": int}', $builder->getArray()]; + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType(new ConstantStringType('"foo"'), new IntegerType()); + yield ['array{\'"foo"\': int}', $builder->getArray()]; + } + + /** + * @dataProvider dataTest + */ + public function testParsingDesiredTypeDescription(string $description, Type $expectedType): void + { + $typeStringResolver = self::getContainer()->getByType(TypeStringResolver::class); + $type = $typeStringResolver->resolve($description); + $this->assertTrue($expectedType->equals($type), sprintf('Parsing %s did not result in %s, but in %s', $description, $expectedType->describe(VerbosityLevel::value()), $type->describe(VerbosityLevel::value()))); + + $newDescription = $type->describe(VerbosityLevel::value()); + $newType = $typeStringResolver->resolve($newDescription); + $this->assertTrue($type->equals($newType), sprintf('Parsing %s again did not result in %s, but in %s', $newDescription, $type->describe(VerbosityLevel::value()), $newType->describe(VerbosityLevel::value()))); + } + + /** + * @dataProvider dataTest + */ + public function testDesiredTypeDescription(string $description, Type $expectedType): void + { + $this->assertSame($description, $expectedType->describe(VerbosityLevel::value())); + + $typeStringResolver = self::getContainer()->getByType(TypeStringResolver::class); + $type = $typeStringResolver->resolve($description); + $this->assertSame($description, $type->describe(VerbosityLevel::value())); + } + +} diff --git a/tests/PHPStan/Process/Runnable/RunnableQueueLoggerStub.php b/tests/PHPStan/Process/Runnable/RunnableQueueLoggerStub.php index 098f2ee4e9..39f6a91833 100644 --- a/tests/PHPStan/Process/Runnable/RunnableQueueLoggerStub.php +++ b/tests/PHPStan/Process/Runnable/RunnableQueueLoggerStub.php @@ -6,7 +6,7 @@ class RunnableQueueLoggerStub implements RunnableQueueLogger { /** @var string[] */ - private $messages = []; + private array $messages = []; /** * @return string[] diff --git a/tests/PHPStan/Process/Runnable/RunnableQueueTest.php b/tests/PHPStan/Process/Runnable/RunnableQueueTest.php index 06aad9aceb..43a94f661c 100644 --- a/tests/PHPStan/Process/Runnable/RunnableQueueTest.php +++ b/tests/PHPStan/Process/Runnable/RunnableQueueTest.php @@ -2,7 +2,9 @@ namespace PHPStan\Process\Runnable; +use Exception; use PHPUnit\Framework\TestCase; +use function sprintf; class RunnableQueueTest extends TestCase { @@ -104,7 +106,7 @@ public function testCancel(): void $promise->then(static function () use ($logger): void { $logger->log('Should not happen'); - }, static function (\Exception $e) use ($logger): void { + }, static function (Exception $e) use ($logger): void { $logger->log(sprintf('Else callback in test called: %s', $e->getMessage())); }); $promise->cancel(); diff --git a/tests/PHPStan/Process/Runnable/RunnableStub.php b/tests/PHPStan/Process/Runnable/RunnableStub.php index d240b3de33..08a3701c90 100644 --- a/tests/PHPStan/Process/Runnable/RunnableStub.php +++ b/tests/PHPStan/Process/Runnable/RunnableStub.php @@ -4,19 +4,15 @@ use React\Promise\CancellablePromiseInterface; use React\Promise\Deferred; +use function sprintf; class RunnableStub implements Runnable { - /** @var string */ - private $name; + private Deferred $deferred; - /** @var Deferred */ - private $deferred; - - public function __construct(string $name) + public function __construct(private string $name) { - $this->name = $name; $this->deferred = new Deferred(); } @@ -38,7 +34,7 @@ public function run(): CancellablePromiseInterface public function cancel(): void { - $this->deferred->reject(new \PHPStan\Process\Runnable\RunnableCanceledException(sprintf('Runnable %s canceled', $this->getName()))); + $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 d585540774..6027b270ac 100644 --- a/tests/PHPStan/Reflection/Annotations/AnnotationsMethodsClassReflectionExtensionTest.php +++ b/tests/PHPStan/Reflection/Annotations/AnnotationsMethodsClassReflectionExtensionTest.php @@ -2,21 +2,29 @@ namespace PHPStan\Reflection\Annotations; +use AnnotationsMethods\Bar; +use AnnotationsMethods\Baz; +use AnnotationsMethods\BazBaz; +use AnnotationsMethods\Foo; +use AnnotationsMethods\FooInterface; use PHPStan\Analyser\Scope; -use PHPStan\Broker\Broker; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\PassedByReference; use PHPStan\Reflection\Php\PhpMethodReflection; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\VerbosityLevel; +use function array_merge; +use function count; +use function sprintf; -class AnnotationsMethodsClassReflectionExtensionTest extends \PHPStan\Testing\TestCase +class AnnotationsMethodsClassReflectionExtensionTest extends PHPStanTestCase { public function dataMethods(): array { $fooMethods = [ 'getInteger' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'int', 'isStatic' => false, 'isVariadic' => false, @@ -38,7 +46,7 @@ public function dataMethods(): array ], ], 'doSomething' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, @@ -60,21 +68,21 @@ public function dataMethods(): array ], ], 'getFooOrBar' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'AnnotationsMethods\Foo', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'methodWithNoReturnType' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'mixed', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getIntegerStatically' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'int', 'isStatic' => true, 'isVariadic' => false, @@ -96,7 +104,7 @@ public function dataMethods(): array ], ], 'doSomethingStatically' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'void', 'isStatic' => true, 'isVariadic' => false, @@ -118,21 +126,21 @@ public function dataMethods(): array ], ], 'getFooOrBarStatically' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'AnnotationsMethods\Foo', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'methodWithNoReturnTypeStatically' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'static(AnnotationsMethods\Foo)', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getIntegerWithDescription' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'int', 'isStatic' => false, 'isVariadic' => false, @@ -154,7 +162,7 @@ public function dataMethods(): array ], ], 'doSomethingWithDescription' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, @@ -176,21 +184,21 @@ public function dataMethods(): array ], ], 'getFooOrBarWithDescription' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'AnnotationsMethods\Foo', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'methodWithNoReturnTypeWithDescription' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'mixed', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getIntegerStaticallyWithDescription' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'int', 'isStatic' => true, 'isVariadic' => false, @@ -212,7 +220,7 @@ public function dataMethods(): array ], ], 'doSomethingStaticallyWithDescription' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'void', 'isStatic' => true, 'isVariadic' => false, @@ -234,154 +242,154 @@ public function dataMethods(): array ], ], 'getFooOrBarStaticallyWithDescription' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'AnnotationsMethods\Foo', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'methodWithNoReturnTypeStaticallyWithDescription' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'static(AnnotationsMethods\Foo)', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'aStaticMethodThatHasAUniqueReturnTypeInThisClass' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'bool', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'aStaticMethodThatHasAUniqueReturnTypeInThisClassWithDescription' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'string', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'getIntegerNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'int', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'doSomethingNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getFooOrBarNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'AnnotationsMethods\Foo', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'methodWithNoReturnTypeNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'mixed', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getIntegerStaticallyNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'int', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'doSomethingStaticallyNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'void', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'getFooOrBarStaticallyNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'AnnotationsMethods\Foo', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'methodWithNoReturnTypeStaticallyNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'static(AnnotationsMethods\Foo)', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getIntegerWithDescriptionNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'int', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'doSomethingWithDescriptionNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getFooOrBarWithDescriptionNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'AnnotationsMethods\Foo', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getIntegerStaticallyWithDescriptionNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'int', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'doSomethingStaticallyWithDescriptionNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'void', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'getFooOrBarStaticallyWithDescriptionNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'AnnotationsMethods\Foo', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'aStaticMethodThatHasAUniqueReturnTypeInThisClassNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'bool|string', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'aStaticMethodThatHasAUniqueReturnTypeInThisClassWithDescriptionNoParams' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'float|string', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'methodFromInterface' => [ - 'class' => \AnnotationsMethods\FooInterface::class, - 'returnType' => \AnnotationsMethods\FooInterface::class, + 'class' => FooInterface::class, + 'returnType' => FooInterface::class, 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'publish' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'Aws\Result', 'isStatic' => false, 'isVariadic' => false, @@ -396,7 +404,7 @@ public function dataMethods(): array ], ], 'rotate' => [ - 'class' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, 'returnType' => 'AnnotationsMethods\Image', 'isStatic' => false, 'isVariadic' => false, @@ -418,15 +426,15 @@ public function dataMethods(): array ], ], 'overridenMethod' => [ - 'class' => \AnnotationsMethods\Foo::class, - 'returnType' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, + 'returnType' => Foo::class, 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'overridenMethodWithAnnotation' => [ - 'class' => \AnnotationsMethods\Foo::class, - 'returnType' => \AnnotationsMethods\Foo::class, + 'class' => Foo::class, + 'returnType' => Foo::class, 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], @@ -436,33 +444,33 @@ public function dataMethods(): array $fooMethods, [ 'overridenMethod' => [ - 'class' => \AnnotationsMethods\Bar::class, - 'returnType' => \AnnotationsMethods\Bar::class, + 'class' => Bar::class, + 'returnType' => Bar::class, 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'overridenMethodWithAnnotation' => [ - 'class' => \AnnotationsMethods\Bar::class, - 'returnType' => \AnnotationsMethods\Bar::class, + 'class' => Bar::class, + 'returnType' => Bar::class, 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'conflictingMethod' => [ - 'class' => \AnnotationsMethods\Bar::class, - 'returnType' => \AnnotationsMethods\Bar::class, + 'class' => Bar::class, + 'returnType' => Bar::class, 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], - ] + ], ); $bazMethods = array_merge( $barMethods, [ 'doSomething' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, @@ -484,7 +492,7 @@ public function dataMethods(): array ], ], 'getIpsum' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'OtherNamespace\Ipsum', 'isStatic' => false, 'isVariadic' => false, @@ -499,7 +507,7 @@ public function dataMethods(): array ], ], 'getIpsumStatically' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'OtherNamespace\Ipsum', 'isStatic' => true, 'isVariadic' => false, @@ -514,7 +522,7 @@ public function dataMethods(): array ], ], 'getIpsumWithDescription' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'OtherNamespace\Ipsum', 'isStatic' => false, 'isVariadic' => false, @@ -529,7 +537,7 @@ public function dataMethods(): array ], ], 'getIpsumStaticallyWithDescription' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'OtherNamespace\Ipsum', 'isStatic' => true, 'isVariadic' => false, @@ -544,7 +552,7 @@ public function dataMethods(): array ], ], 'doSomethingStatically' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'void', 'isStatic' => true, 'isVariadic' => false, @@ -566,7 +574,7 @@ public function dataMethods(): array ], ], 'doSomethingWithDescription' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, @@ -588,7 +596,7 @@ public function dataMethods(): array ], ], 'doSomethingStaticallyWithDescription' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'void', 'isStatic' => true, 'isVariadic' => false, @@ -610,75 +618,75 @@ public function dataMethods(): array ], ], 'doSomethingNoParams' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'doSomethingStaticallyNoParams' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'void', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'doSomethingWithDescriptionNoParams' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'doSomethingStaticallyWithDescriptionNoParams' => [ - 'class' => \AnnotationsMethods\Baz::class, + 'class' => Baz::class, 'returnType' => 'void', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'methodFromTrait' => [ - 'class' => \AnnotationsMethods\Baz::class, - 'returnType' => \AnnotationsMethods\BazBaz::class, + 'class' => Baz::class, + 'returnType' => BazBaz::class, 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], - ] + ], ); $bazBazMethods = array_merge( $bazMethods, [ 'getTest' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'OtherNamespace\Test', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getTestStatically' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'OtherNamespace\Test', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'getTestWithDescription' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'OtherNamespace\Test', 'isStatic' => false, 'isVariadic' => false, 'parameters' => [], ], 'getTestStaticallyWithDescription' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'OtherNamespace\Test', 'isStatic' => true, 'isVariadic' => false, 'parameters' => [], ], 'doSomethingWithSpecificScalarParamsWithoutDefault' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, @@ -714,7 +722,7 @@ public function dataMethods(): array ], ], 'doSomethingWithSpecificScalarParamsWithDefault' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, @@ -750,7 +758,7 @@ public function dataMethods(): array ], ], 'doSomethingWithSpecificObjectParamsWithoutDefault' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, @@ -786,7 +794,7 @@ public function dataMethods(): array ], ], 'doSomethingWithSpecificObjectParamsWithDefault' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, @@ -822,7 +830,7 @@ public function dataMethods(): array ], ], 'doSomethingWithSpecificVariadicScalarParamsNotNullable' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => true, @@ -837,7 +845,7 @@ public function dataMethods(): array ], ], 'doSomethingWithSpecificVariadicScalarParamsNullable' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => true, @@ -852,7 +860,7 @@ public function dataMethods(): array ], ], 'doSomethingWithSpecificVariadicObjectParamsNotNullable' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => true, @@ -867,7 +875,7 @@ public function dataMethods(): array ], ], 'doSomethingWithSpecificVariadicObjectParamsNullable' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => true, @@ -882,7 +890,7 @@ public function dataMethods(): array ], ], 'doSomethingWithComplicatedParameters' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'void', 'isStatic' => false, 'isVariadic' => false, @@ -918,7 +926,7 @@ public function dataMethods(): array ], ], 'paramMultipleTypesWithExtraSpaces' => [ - 'class' => \AnnotationsMethods\BazBaz::class, + 'class' => BazBaz::class, 'returnType' => 'float|int', 'isStatic' => false, 'isVariadic' => false, @@ -939,27 +947,25 @@ public function dataMethods(): array ], ], ], - ] + ], ); return [ - [\AnnotationsMethods\Foo::class, $fooMethods], - [\AnnotationsMethods\Bar::class, $barMethods], - [\AnnotationsMethods\Baz::class, $bazMethods], - [\AnnotationsMethods\BazBaz::class, $bazBazMethods], + [Foo::class, $fooMethods], + [Bar::class, $barMethods], + [Baz::class, $bazMethods], + [BazBaz::class, $bazBazMethods], ]; } /** * @dataProvider dataMethods - * @param string $className * @param array $methods */ public function testMethods(string $className, array $methods): void { - /** @var Broker $broker */ - $broker = self::getContainer()->getByType(Broker::class); - $class = $broker->getClass($className); + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass($className); $scope = $this->createMock(Scope::class); $scope->method('isInClass')->willReturn(true); $scope->method('getClassReflection')->willReturn($class); @@ -972,47 +978,47 @@ public function testMethods(string $className, array $methods): void $this->assertSame( $expectedMethodData['class'], $method->getDeclaringClass()->getName(), - sprintf('Declaring class of method %s() does not match.', $methodName) + sprintf('Declaring class of method %s() does not match.', $methodName), ); $this->assertSame( $expectedMethodData['returnType'], $selectedParametersAcceptor->getReturnType()->describe(VerbosityLevel::precise()), - sprintf('Return type of method %s::%s() does not match', $className, $methodName) + sprintf('Return type of method %s::%s() does not match', $className, $methodName), ); $this->assertSame( $expectedMethodData['isStatic'], $method->isStatic(), - sprintf('Scope of method %s::%s() does not match', $className, $methodName) + sprintf('Scope of method %s::%s() does not match', $className, $methodName), ); $this->assertSame( $expectedMethodData['isVariadic'], $selectedParametersAcceptor->isVariadic(), - sprintf('Method %s::%s() does not match expected variadicity', $className, $methodName) + sprintf('Method %s::%s() does not match expected variadicity', $className, $methodName), ); $this->assertCount( count($expectedMethodData['parameters']), $selectedParametersAcceptor->getParameters(), - sprintf('Method %s::%s() does not match expected count of parameters', $className, $methodName) + sprintf('Method %s::%s() does not match expected count of parameters', $className, $methodName), ); foreach ($selectedParametersAcceptor->getParameters() as $i => $parameter) { $this->assertSame( $expectedMethodData['parameters'][$i]['name'], - $parameter->getName() + $parameter->getName(), ); $this->assertSame( $expectedMethodData['parameters'][$i]['type'], - $parameter->getType()->describe(VerbosityLevel::precise()) + $parameter->getType()->describe(VerbosityLevel::precise()), ); $this->assertTrue( - $expectedMethodData['parameters'][$i]['passedByReference']->equals($parameter->passedByReference()) + $expectedMethodData['parameters'][$i]['passedByReference']->equals($parameter->passedByReference()), ); $this->assertSame( $expectedMethodData['parameters'][$i]['isOptional'], - $parameter->isOptional() + $parameter->isOptional(), ); $this->assertSame( $expectedMethodData['parameters'][$i]['isVariadic'], - $parameter->isVariadic() + $parameter->isVariadic(), ); } } @@ -1020,8 +1026,8 @@ public function testMethods(string $className, array $methods): void public function testOverridingNativeMethodsWithAnnotationsDoesNotBreakGetNativeMethod(): void { - $broker = self::getContainer()->getByType(Broker::class); - $class = $broker->getClass(\AnnotationsMethods\Bar::class); + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass(Bar::class); $this->assertTrue($class->hasNativeMethod('overridenMethodWithAnnotation')); $this->assertInstanceOf(PhpMethodReflection::class, $class->getNativeMethod('overridenMethodWithAnnotation')); } diff --git a/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php b/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php index 0291842501..14dabbece3 100644 --- a/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php +++ b/tests/PHPStan/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtensionTest.php @@ -2,151 +2,157 @@ namespace PHPStan\Reflection\Annotations; +use AnnotationsProperties\Bar; +use AnnotationsProperties\Baz; +use AnnotationsProperties\BazBaz; +use AnnotationsProperties\Foo; +use AnnotationsProperties\FooInterface; use PHPStan\Analyser\Scope; -use PHPStan\Broker\Broker; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\VerbosityLevel; +use function sprintf; -class AnnotationsPropertiesClassReflectionExtensionTest extends \PHPStan\Testing\TestCase +class AnnotationsPropertiesClassReflectionExtensionTest extends PHPStanTestCase { public function dataProperties(): array { return [ [ - \AnnotationsProperties\Foo::class, + Foo::class, [ 'otherTest' => [ - 'class' => \AnnotationsProperties\Foo::class, + 'class' => Foo::class, 'type' => 'OtherNamespace\Test', 'writable' => true, 'readable' => true, ], 'otherTestReadOnly' => [ - 'class' => \AnnotationsProperties\Foo::class, + 'class' => Foo::class, 'type' => 'OtherNamespace\Ipsum', 'writable' => false, 'readable' => true, ], 'fooOrBar' => [ - 'class' => \AnnotationsProperties\Foo::class, + 'class' => Foo::class, 'type' => 'AnnotationsProperties\Foo', 'writable' => true, 'readable' => true, ], 'conflictingProperty' => [ - 'class' => \AnnotationsProperties\Foo::class, + 'class' => Foo::class, 'type' => 'OtherNamespace\Ipsum', 'writable' => true, 'readable' => true, ], 'interfaceProperty' => [ - 'class' => \AnnotationsProperties\FooInterface::class, - 'type' => \AnnotationsProperties\FooInterface::class, + 'class' => FooInterface::class, + 'type' => FooInterface::class, 'writable' => true, 'readable' => true, ], 'overridenProperty' => [ - 'class' => \AnnotationsProperties\Foo::class, - 'type' => \AnnotationsProperties\Foo::class, + 'class' => Foo::class, + 'type' => Foo::class, 'writable' => true, 'readable' => true, ], 'overridenPropertyWithAnnotation' => [ - 'class' => \AnnotationsProperties\Foo::class, - 'type' => \AnnotationsProperties\Foo::class, + 'class' => Foo::class, + 'type' => Foo::class, 'writable' => true, 'readable' => true, ], ], ], [ - \AnnotationsProperties\Bar::class, + Bar::class, [ 'otherTest' => [ - 'class' => \AnnotationsProperties\Foo::class, + 'class' => Foo::class, 'type' => 'OtherNamespace\Test', 'writable' => true, 'readable' => true, ], 'otherTestReadOnly' => [ - 'class' => \AnnotationsProperties\Foo::class, + 'class' => Foo::class, 'type' => 'OtherNamespace\Ipsum', 'writable' => false, 'readable' => true, ], 'fooOrBar' => [ - 'class' => \AnnotationsProperties\Foo::class, + 'class' => Foo::class, 'type' => 'AnnotationsProperties\Foo', 'writable' => true, 'readable' => true, ], 'conflictingProperty' => [ - 'class' => \AnnotationsProperties\Foo::class, + 'class' => Foo::class, 'type' => 'OtherNamespace\Ipsum', 'writable' => true, 'readable' => true, ], 'overridenProperty' => [ - 'class' => \AnnotationsProperties\Bar::class, - 'type' => \AnnotationsProperties\Bar::class, + 'class' => Bar::class, + 'type' => Bar::class, 'writable' => true, 'readable' => true, ], 'overridenPropertyWithAnnotation' => [ - 'class' => \AnnotationsProperties\Bar::class, - 'type' => \AnnotationsProperties\Bar::class, + 'class' => Bar::class, + 'type' => Bar::class, 'writable' => true, 'readable' => true, ], 'conflictingAnnotationProperty' => [ - 'class' => \AnnotationsProperties\Bar::class, - 'type' => \AnnotationsProperties\Bar::class, + 'class' => Bar::class, + 'type' => Bar::class, 'writable' => true, 'readable' => true, ], ], ], [ - \AnnotationsProperties\Baz::class, + Baz::class, [ 'otherTest' => [ - 'class' => \AnnotationsProperties\Foo::class, + 'class' => Foo::class, 'type' => 'OtherNamespace\Test', 'writable' => true, 'readable' => true, ], 'otherTestReadOnly' => [ - 'class' => \AnnotationsProperties\Foo::class, + 'class' => Foo::class, 'type' => 'OtherNamespace\Ipsum', 'writable' => false, 'readable' => true, ], 'fooOrBar' => [ - 'class' => \AnnotationsProperties\Foo::class, + 'class' => Foo::class, 'type' => 'AnnotationsProperties\Foo', 'writable' => true, 'readable' => true, ], 'conflictingProperty' => [ - 'class' => \AnnotationsProperties\Baz::class, + 'class' => Baz::class, 'type' => 'AnnotationsProperties\Dolor', 'writable' => true, 'readable' => true, ], 'bazProperty' => [ - 'class' => \AnnotationsProperties\Baz::class, + 'class' => Baz::class, 'type' => 'AnnotationsProperties\Lorem', 'writable' => true, 'readable' => true, ], 'traitProperty' => [ - 'class' => \AnnotationsProperties\Baz::class, + 'class' => Baz::class, 'type' => 'AnnotationsProperties\BazBaz', 'writable' => true, 'readable' => true, ], 'writeOnlyProperty' => [ - 'class' => \AnnotationsProperties\Baz::class, + 'class' => Baz::class, 'type' => 'AnnotationsProperties\Lorem|null', 'writable' => true, 'readable' => false, @@ -154,52 +160,52 @@ public function dataProperties(): array ], ], [ - \AnnotationsProperties\BazBaz::class, + BazBaz::class, [ 'otherTest' => [ - 'class' => \AnnotationsProperties\Foo::class, + 'class' => Foo::class, 'type' => 'OtherNamespace\Test', 'writable' => true, 'readable' => true, ], 'otherTestReadOnly' => [ - 'class' => \AnnotationsProperties\Foo::class, + 'class' => Foo::class, 'type' => 'OtherNamespace\Ipsum', 'writable' => false, 'readable' => true, ], 'fooOrBar' => [ - 'class' => \AnnotationsProperties\Foo::class, + 'class' => Foo::class, 'type' => 'AnnotationsProperties\Foo', 'writable' => true, 'readable' => true, ], 'conflictingProperty' => [ - 'class' => \AnnotationsProperties\Baz::class, + 'class' => Baz::class, 'type' => 'AnnotationsProperties\Dolor', 'writable' => true, 'readable' => true, ], 'bazProperty' => [ - 'class' => \AnnotationsProperties\Baz::class, + 'class' => Baz::class, 'type' => 'AnnotationsProperties\Lorem', 'writable' => true, 'readable' => true, ], 'traitProperty' => [ - 'class' => \AnnotationsProperties\Baz::class, + 'class' => Baz::class, 'type' => 'AnnotationsProperties\BazBaz', 'writable' => true, 'readable' => true, ], 'writeOnlyProperty' => [ - 'class' => \AnnotationsProperties\Baz::class, + 'class' => Baz::class, 'type' => 'AnnotationsProperties\Lorem|null', 'writable' => true, 'readable' => false, ], 'numericBazBazProperty' => [ - 'class' => \AnnotationsProperties\BazBaz::class, + 'class' => BazBaz::class, 'type' => 'float|int', 'writable' => true, 'readable' => true, @@ -211,14 +217,12 @@ public function dataProperties(): array /** * @dataProvider dataProperties - * @param string $className * @param array $properties */ public function testProperties(string $className, array $properties): void { - /** @var Broker $broker */ - $broker = self::getContainer()->getByType(Broker::class); - $class = $broker->getClass($className); + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass($className); $scope = $this->createMock(Scope::class); $scope->method('isInClass')->willReturn(true); $scope->method('getClassReflection')->willReturn($class); @@ -226,37 +230,37 @@ public function testProperties(string $className, array $properties): void foreach ($properties as $propertyName => $expectedPropertyData) { $this->assertTrue( $class->hasProperty($propertyName), - sprintf('Class %s does not define property %s.', $className, $propertyName) + sprintf('Class %s does not define property %s.', $className, $propertyName), ); $property = $class->getProperty($propertyName, $scope); $this->assertSame( $expectedPropertyData['class'], $property->getDeclaringClass()->getName(), - sprintf('Declaring class of property $%s does not match.', $propertyName) + sprintf('Declaring class of property $%s does not match.', $propertyName), ); $this->assertSame( $expectedPropertyData['type'], $property->getReadableType()->describe(VerbosityLevel::precise()), - sprintf('Type of property %s::$%s does not match.', $property->getDeclaringClass()->getName(), $propertyName) + sprintf('Type of property %s::$%s does not match.', $property->getDeclaringClass()->getName(), $propertyName), ); $this->assertSame( $expectedPropertyData['readable'], $property->isReadable(), - sprintf('Property %s::$%s readability is not as expected.', $property->getDeclaringClass()->getName(), $propertyName) + sprintf('Property %s::$%s readability is not as expected.', $property->getDeclaringClass()->getName(), $propertyName), ); $this->assertSame( $expectedPropertyData['writable'], $property->isWritable(), - sprintf('Property %s::$%s writability is not as expected.', $property->getDeclaringClass()->getName(), $propertyName) + sprintf('Property %s::$%s writability is not as expected.', $property->getDeclaringClass()->getName(), $propertyName), ); } } public function testOverridingNativePropertiesWithAnnotationsDoesNotBreakGetNativeProperty(): void { - $broker = self::getContainer()->getByType(Broker::class); - $class = $broker->getClass(\AnnotationsProperties\Bar::class); + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass(Bar::class); $this->assertTrue($class->hasNativeProperty('overridenPropertyWithAnnotation')); $this->assertSame('AnnotationsProperties\Foo', $class->getNativeProperty('overridenPropertyWithAnnotation')->getReadableType()->describe(VerbosityLevel::precise())); } diff --git a/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php b/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php index 6ffdc2fee1..2d108fb0c9 100644 --- a/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php +++ b/tests/PHPStan/Reflection/Annotations/DeprecatedAnnotationsTest.php @@ -2,11 +2,16 @@ namespace PHPStan\Reflection\Annotations; +use DeprecatedAnnotations\DeprecatedBar; +use DeprecatedAnnotations\DeprecatedFoo; +use DeprecatedAnnotations\DeprecatedWithMultipleTags; +use DeprecatedAnnotations\Foo; +use DeprecatedAnnotations\FooInterface; use PhpParser\Node\Name; use PHPStan\Analyser\Scope; -use PHPStan\Broker\Broker; +use PHPStan\Testing\PHPStanTestCase; -class DeprecatedAnnotationsTest extends \PHPStan\Testing\TestCase +class DeprecatedAnnotationsTest extends PHPStanTestCase { public function dataDeprecatedAnnotations(): array @@ -14,7 +19,7 @@ public function dataDeprecatedAnnotations(): array return [ [ false, - \DeprecatedAnnotations\Foo::class, + Foo::class, null, [ 'constant' => [ @@ -32,7 +37,7 @@ public function dataDeprecatedAnnotations(): array ], [ true, - \DeprecatedAnnotations\DeprecatedFoo::class, + DeprecatedFoo::class, 'in 1.0.0.', [ 'constant' => [ @@ -50,7 +55,7 @@ public function dataDeprecatedAnnotations(): array ], [ false, - \DeprecatedAnnotations\FooInterface::class, + FooInterface::class, null, [ 'constant' => [ @@ -64,7 +69,7 @@ public function dataDeprecatedAnnotations(): array ], [ true, - \DeprecatedAnnotations\DeprecatedWithMultipleTags::class, + DeprecatedWithMultipleTags::class, "in Foo 1.1.0 and will be removed in 1.5.0, use\n \\Foo\\Bar\\NotDeprecated instead.", [ 'method' => [ @@ -77,16 +82,12 @@ public function dataDeprecatedAnnotations(): array /** * @dataProvider dataDeprecatedAnnotations - * @param bool $deprecated - * @param string $className - * @param string|null $classDeprecation * @param array $deprecatedAnnotations */ public function testDeprecatedAnnotations(bool $deprecated, string $className, ?string $classDeprecation, array $deprecatedAnnotations): void { - /** @var Broker $broker */ - $broker = self::getContainer()->getByType(Broker::class); - $class = $broker->getClass($className); + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass($className); $scope = $this->createMock(Scope::class); $scope->method('isInClass')->willReturn(true); $scope->method('getClassReflection')->willReturn($class); @@ -118,21 +119,26 @@ public function testDeprecatedUserFunctions(): void { require_once __DIR__ . '/data/annotations-deprecated.php'; - /** @var Broker $broker */ - $broker = self::getContainer()->getByType(Broker::class); + $reflectionProvider = $this->createReflectionProvider(); - $this->assertFalse($broker->getFunction(new Name\FullyQualified('DeprecatedAnnotations\foo'), null)->isDeprecated()->yes()); - $this->assertTrue($broker->getFunction(new Name\FullyQualified('DeprecatedAnnotations\deprecatedFoo'), null)->isDeprecated()->yes()); + $this->assertFalse($reflectionProvider->getFunction(new Name\FullyQualified('DeprecatedAnnotations\foo'), null)->isDeprecated()->yes()); + $this->assertTrue($reflectionProvider->getFunction(new Name\FullyQualified('DeprecatedAnnotations\deprecatedFoo'), null)->isDeprecated()->yes()); } public function testNonDeprecatedNativeFunctions(): void { - /** @var Broker $broker */ - $broker = self::getContainer()->getByType(Broker::class); + $reflectionProvider = $this->createReflectionProvider(); - $this->assertFalse($broker->getFunction(new Name('str_replace'), null)->isDeprecated()->yes()); - $this->assertFalse($broker->getFunction(new Name('get_class'), null)->isDeprecated()->yes()); - $this->assertFalse($broker->getFunction(new Name('function_exists'), null)->isDeprecated()->yes()); + $this->assertFalse($reflectionProvider->getFunction(new Name('str_replace'), null)->isDeprecated()->yes()); + $this->assertFalse($reflectionProvider->getFunction(new Name('get_class'), null)->isDeprecated()->yes()); + $this->assertFalse($reflectionProvider->getFunction(new Name('function_exists'), null)->isDeprecated()->yes()); + } + + public function testDeprecatedMethodsFromInterface(): void + { + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass(DeprecatedBar::class); + $this->assertTrue($class->getNativeMethod('superDeprecated')->isDeprecated()->yes()); } } diff --git a/tests/PHPStan/Reflection/Annotations/FinalAnnotationsTest.php b/tests/PHPStan/Reflection/Annotations/FinalAnnotationsTest.php index d876b4765f..fb61a5c90e 100644 --- a/tests/PHPStan/Reflection/Annotations/FinalAnnotationsTest.php +++ b/tests/PHPStan/Reflection/Annotations/FinalAnnotationsTest.php @@ -2,11 +2,13 @@ namespace PHPStan\Reflection\Annotations; +use FinalAnnotations\FinalFoo; +use FinalAnnotations\Foo; use PhpParser\Node\Name; use PHPStan\Analyser\Scope; -use PHPStan\Broker\Broker; +use PHPStan\Testing\PHPStanTestCase; -class FinalAnnotationsTest extends \PHPStan\Testing\TestCase +class FinalAnnotationsTest extends PHPStanTestCase { public function dataFinalAnnotations(): array @@ -14,7 +16,7 @@ public function dataFinalAnnotations(): array return [ [ false, - \FinalAnnotations\Foo::class, + Foo::class, [ 'method' => [ 'foo', @@ -24,7 +26,7 @@ public function dataFinalAnnotations(): array ], [ true, - \FinalAnnotations\FinalFoo::class, + FinalFoo::class, [ 'method' => [ 'finalFoo', @@ -37,15 +39,12 @@ public function dataFinalAnnotations(): array /** * @dataProvider dataFinalAnnotations - * @param bool $final - * @param string $className * @param array $finalAnnotations */ public function testFinalAnnotations(bool $final, string $className, array $finalAnnotations): void { - /** @var Broker $broker */ - $broker = self::getContainer()->getByType(Broker::class); - $class = $broker->getClass($className); + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass($className); $scope = $this->createMock(Scope::class); $scope->method('isInClass')->willReturn(true); $scope->method('getClassReflection')->willReturn($class); @@ -63,11 +62,10 @@ public function testFinalUserFunctions(): void { require_once __DIR__ . '/data/annotations-final.php'; - /** @var Broker $broker */ - $broker = self::getContainer()->getByType(Broker::class); + $reflectionProvider = $this->createReflectionProvider(); - $this->assertFalse($broker->getFunction(new Name\FullyQualified('FinalAnnotations\foo'), null)->isFinal()->yes()); - $this->assertTrue($broker->getFunction(new Name\FullyQualified('FinalAnnotations\finalFoo'), null)->isFinal()->yes()); + $this->assertFalse($reflectionProvider->getFunction(new Name\FullyQualified('FinalAnnotations\foo'), null)->isFinal()->yes()); + $this->assertTrue($reflectionProvider->getFunction(new Name\FullyQualified('FinalAnnotations\finalFoo'), null)->isFinal()->yes()); } } diff --git a/tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php b/tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php index 095001f84b..b734fac05c 100644 --- a/tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php +++ b/tests/PHPStan/Reflection/Annotations/InternalAnnotationsTest.php @@ -2,11 +2,17 @@ namespace PHPStan\Reflection\Annotations; +use InternalAnnotations\Foo; +use InternalAnnotations\FooInterface; +use InternalAnnotations\FooTrait; +use InternalAnnotations\InternalFoo; +use InternalAnnotations\InternalFooInterface; +use InternalAnnotations\InternalFooTrait; use PhpParser\Node\Name; use PHPStan\Analyser\Scope; -use PHPStan\Broker\Broker; +use PHPStan\Testing\PHPStanTestCase; -class InternalAnnotationsTest extends \PHPStan\Testing\TestCase +class InternalAnnotationsTest extends PHPStanTestCase { public function dataInternalAnnotations(): array @@ -14,7 +20,7 @@ public function dataInternalAnnotations(): array return [ [ false, - \InternalAnnotations\Foo::class, + Foo::class, [ 'constant' => [ 'FOO', @@ -31,7 +37,7 @@ public function dataInternalAnnotations(): array ], [ true, - \InternalAnnotations\InternalFoo::class, + InternalFoo::class, [ 'constant' => [ 'INTERNAL_FOO', @@ -48,7 +54,7 @@ public function dataInternalAnnotations(): array ], [ false, - \InternalAnnotations\FooInterface::class, + FooInterface::class, [ 'constant' => [ 'FOO', @@ -61,7 +67,7 @@ public function dataInternalAnnotations(): array ], [ true, - \InternalAnnotations\InternalFooInterface::class, + InternalFooInterface::class, [ 'constant' => [ 'INTERNAL_FOO', @@ -74,7 +80,7 @@ public function dataInternalAnnotations(): array ], [ false, - \InternalAnnotations\FooTrait::class, + FooTrait::class, [ 'method' => [ 'foo', @@ -88,7 +94,7 @@ public function dataInternalAnnotations(): array ], [ true, - \InternalAnnotations\InternalFooTrait::class, + InternalFooTrait::class, [ 'method' => [ 'internalFoo', @@ -105,15 +111,12 @@ public function dataInternalAnnotations(): array /** * @dataProvider dataInternalAnnotations - * @param bool $internal - * @param string $className * @param array $internalAnnotations */ public function testInternalAnnotations(bool $internal, string $className, array $internalAnnotations): void { - /** @var Broker $broker */ - $broker = self::getContainer()->getByType(Broker::class); - $class = $broker->getClass($className); + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass($className); $scope = $this->createMock(Scope::class); $scope->method('isInClass')->willReturn(true); $scope->method('getClassReflection')->willReturn($class); @@ -141,11 +144,10 @@ public function testInternalUserFunctions(): void { require_once __DIR__ . '/data/annotations-internal.php'; - /** @var Broker $broker */ - $broker = self::getContainer()->getByType(Broker::class); + $reflectionProvider = $this->createReflectionProvider(); - $this->assertFalse($broker->getFunction(new Name\FullyQualified('InternalAnnotations\foo'), null)->isInternal()->yes()); - $this->assertTrue($broker->getFunction(new Name\FullyQualified('InternalAnnotations\internalFoo'), null)->isInternal()->yes()); + $this->assertFalse($reflectionProvider->getFunction(new Name\FullyQualified('InternalAnnotations\foo'), null)->isInternal()->yes()); + $this->assertTrue($reflectionProvider->getFunction(new Name\FullyQualified('InternalAnnotations\internalFoo'), null)->isInternal()->yes()); } } diff --git a/tests/PHPStan/Reflection/Annotations/ThrowsAnnotationsTest.php b/tests/PHPStan/Reflection/Annotations/ThrowsAnnotationsTest.php index 4e1de7a891..7e3f56b299 100644 --- a/tests/PHPStan/Reflection/Annotations/ThrowsAnnotationsTest.php +++ b/tests/PHPStan/Reflection/Annotations/ThrowsAnnotationsTest.php @@ -4,57 +4,63 @@ use PhpParser\Node\Name; use PHPStan\Analyser\Scope; -use PHPStan\Broker\Broker; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\VerbosityLevel; - -class ThrowsAnnotationsTest extends \PHPStan\Testing\TestCase +use RuntimeException; +use ThrowsAnnotations\BarTrait; +use ThrowsAnnotations\Foo; +use ThrowsAnnotations\FooInterface; +use ThrowsAnnotations\FooTrait; +use ThrowsAnnotations\PhpstanFoo; + +class ThrowsAnnotationsTest extends PHPStanTestCase { public function dataThrowsAnnotations(): array { return [ [ - \ThrowsAnnotations\Foo::class, + Foo::class, [ 'withoutThrows' => null, - 'throwsRuntime' => \RuntimeException::class, - 'staticThrowsRuntime' => \RuntimeException::class, + 'throwsRuntime' => RuntimeException::class, + 'staticThrowsRuntime' => RuntimeException::class, ], ], [ - \ThrowsAnnotations\PhpstanFoo::class, + PhpstanFoo::class, [ 'withoutThrows' => 'void', - 'throwsRuntime' => \RuntimeException::class, - 'staticThrowsRuntime' => \RuntimeException::class, + 'throwsRuntime' => RuntimeException::class, + 'staticThrowsRuntime' => RuntimeException::class, ], ], [ - \ThrowsAnnotations\FooInterface::class, + FooInterface::class, [ 'withoutThrows' => null, - 'throwsRuntime' => \RuntimeException::class, - 'staticThrowsRuntime' => \RuntimeException::class, + 'throwsRuntime' => RuntimeException::class, + 'staticThrowsRuntime' => RuntimeException::class, ], ], [ - \ThrowsAnnotations\FooTrait::class, + FooTrait::class, [ 'withoutThrows' => null, - 'throwsRuntime' => \RuntimeException::class, - 'staticThrowsRuntime' => \RuntimeException::class, + 'throwsRuntime' => RuntimeException::class, + 'staticThrowsRuntime' => RuntimeException::class, ], ], [ - \ThrowsAnnotations\BarTrait::class, + BarTrait::class, [ 'withoutThrows' => null, - 'throwsRuntime' => \RuntimeException::class, - 'staticThrowsRuntime' => \RuntimeException::class, + 'throwsRuntime' => RuntimeException::class, + 'staticThrowsRuntime' => RuntimeException::class, ], ], @@ -63,14 +69,12 @@ public function dataThrowsAnnotations(): array /** * @dataProvider dataThrowsAnnotations - * @param string $className * @param array $throwsAnnotations */ public function testThrowsAnnotations(string $className, array $throwsAnnotations): void { - /** @var Broker $broker */ - $broker = self::getContainer()->getByType(Broker::class); - $class = $broker->getClass($className); + $reflectionProvider = $this->createReflectionProvider(); + $class = $reflectionProvider->getClass($className); $scope = $this->createMock(Scope::class); foreach ($throwsAnnotations as $methodName => $type) { @@ -84,14 +88,13 @@ public function testThrowsOnUserFunctions(): void { require_once __DIR__ . '/data/annotations-throws.php'; - /** @var Broker $broker */ - $broker = self::getContainer()->getByType(Broker::class); + $reflectionProvider = $this->createReflectionProvider(); - $this->assertNull($broker->getFunction(new Name\FullyQualified('ThrowsAnnotations\withoutThrows'), null)->getThrowType()); + $this->assertNull($reflectionProvider->getFunction(new Name\FullyQualified('ThrowsAnnotations\withoutThrows'), null)->getThrowType()); - $throwType = $broker->getFunction(new Name\FullyQualified('ThrowsAnnotations\throwsRuntime'), null)->getThrowType(); + $throwType = $reflectionProvider->getFunction(new Name\FullyQualified('ThrowsAnnotations\throwsRuntime'), null)->getThrowType(); $this->assertNotNull($throwType); - $this->assertSame(\RuntimeException::class, $throwType->describe(VerbosityLevel::typeOnly())); + $this->assertSame(RuntimeException::class, $throwType->describe(VerbosityLevel::typeOnly())); } } diff --git a/tests/PHPStan/Reflection/Annotations/data/annotations-deprecated.php b/tests/PHPStan/Reflection/Annotations/data/annotations-deprecated.php index 0d4db04b06..1095002d92 100644 --- a/tests/PHPStan/Reflection/Annotations/data/annotations-deprecated.php +++ b/tests/PHPStan/Reflection/Annotations/data/annotations-deprecated.php @@ -187,3 +187,31 @@ public function deprecatedFoo() { } } + +/** + * @deprecated This is totally deprecated. + */ +interface BarInterface +{ + + /** + * @deprecated This is totally deprecated. + */ + public function superDeprecated(); + +} + +/** + * {@inheritdoc} + */ +class DeprecatedBar implements BarInterface +{ + + /** + * {@inheritdoc} + */ + public function superDeprecated() + { + } + +} diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocatorTest.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocatorTest.php index 6d37cd64fd..57442ab9ba 100644 --- a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocatorTest.php +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocatorTest.php @@ -3,10 +3,8 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; use PHPStan\BetterReflection\Reflection\ReflectionClass; -use PHPStan\BetterReflection\Reflector\ClassReflector; -use PHPStan\BetterReflection\Reflector\ConstantReflector; -use PHPStan\BetterReflection\Reflector\FunctionReflector; -use PHPStan\Testing\TestCase; +use PHPStan\BetterReflection\Reflector\DefaultReflector; +use PHPStan\Testing\PHPStanTestCase; use TestSingleFileSourceLocator\AFoo; use TestSingleFileSourceLocator\InCondition; @@ -15,41 +13,37 @@ function testFunctionForLocator(): void // phpcs:disable } -class AutoloadSourceLocatorTest extends TestCase +class AutoloadSourceLocatorTest extends PHPStanTestCase { public function testAutoloadEverythingInFile(): void { - /** @var FunctionReflector $functionReflector */ - $functionReflector = null; - $locator = new AutoloadSourceLocator(self::getContainer()->getByType(FileNodesFetcher::class)); - $classReflector = new ClassReflector($locator); - $functionReflector = new FunctionReflector($locator, $classReflector); - $constantReflector = new ConstantReflector($locator, $classReflector); - $aFoo = $classReflector->reflect(AFoo::class); + $locator = new AutoloadSourceLocator(self::getContainer()->getByType(FileNodesFetcher::class), false); + $reflector = new DefaultReflector($locator); + $aFoo = $reflector->reflectClass(AFoo::class); $this->assertNotNull($aFoo->getFileName()); $this->assertSame('a.php', basename($aFoo->getFileName())); - $testFunctionReflection = $functionReflector->reflect('PHPStan\\Reflection\\BetterReflection\\SourceLocator\testFunctionForLocator'); + $testFunctionReflection = $reflector->reflectFunction('PHPStan\\Reflection\\BetterReflection\\SourceLocator\testFunctionForLocator'); $this->assertSame(str_replace('\\', '/', __FILE__), $testFunctionReflection->getFileName()); - $someConstant = $constantReflector->reflect('TestSingleFileSourceLocator\\SOME_CONSTANT'); + $someConstant = $reflector->reflectConstant('TestSingleFileSourceLocator\\SOME_CONSTANT'); $this->assertNotNull($someConstant->getFileName()); $this->assertSame('a.php', basename($someConstant->getFileName())); $this->assertSame(1, $someConstant->getValue()); - $anotherConstant = $constantReflector->reflect('TestSingleFileSourceLocator\\ANOTHER_CONSTANT'); + $anotherConstant = $reflector->reflectConstant('TestSingleFileSourceLocator\\ANOTHER_CONSTANT'); $this->assertNotNull($anotherConstant->getFileName()); $this->assertSame('a.php', basename($anotherConstant->getFileName())); $this->assertSame(2, $anotherConstant->getValue()); - $doFooFunctionReflection = $functionReflector->reflect('TestSingleFileSourceLocator\\doFoo'); + $doFooFunctionReflection = $reflector->reflectFunction('TestSingleFileSourceLocator\\doFoo'); $this->assertSame('TestSingleFileSourceLocator\\doFoo', $doFooFunctionReflection->getName()); $this->assertNotNull($doFooFunctionReflection->getFileName()); $this->assertSame('a.php', basename($doFooFunctionReflection->getFileName())); class_exists(InCondition::class); - $classInCondition = $classReflector->reflect(InCondition::class); + $classInCondition = $reflector->reflectClass(InCondition::class); $classInConditionFilename = $classInCondition->getFileName(); $this->assertNotNull($classInConditionFilename); $this->assertSame('a.php', basename($classInConditionFilename)); diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorTest.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorTest.php index cce937f43d..8a853c48f4 100644 --- a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorTest.php +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedDirectorySourceLocatorTest.php @@ -2,18 +2,20 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; -use PHPStan\BetterReflection\Reflector\ClassReflector; +use OptimizedDirectory\BFoo; +use PHPStan\BetterReflection\Reflector\DefaultReflector; use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; -use PHPStan\BetterReflection\Reflector\FunctionReflector; -use PHPStan\Testing\TestCase; +use PHPStan\Testing\PHPStanTestCase; use TestDirectorySourceLocator\AFoo; +use function basename; +use const PHP_VERSION_ID; -class OptimizedDirectorySourceLocatorTest extends TestCase +class OptimizedDirectorySourceLocatorTest extends PHPStanTestCase { - public function dataClass(): array + public function dataClass(): iterable { - return [ + yield from [ [ AFoo::class, AFoo::class, @@ -25,29 +27,49 @@ public function dataClass(): array 'a.php', ], [ - \BFoo::class, - \BFoo::class, + BFoo::class, + BFoo::class, 'b.php', ], [ - 'bfOO', - \BFoo::class, + 'OptimizedDirectory\\bfOO', + BFoo::class, 'b.php', ], ]; + + if (PHP_VERSION_ID < 80100) { + return; + } + + yield [ + 'OptimizedDirectory\\TestEnum', + 'OptimizedDirectory\\TestEnum', + 'enum.php', + ]; + + yield [ + 'OptimizedDirectory\\BackedByStringWithoutSpace', + 'OptimizedDirectory\\BackedByStringWithoutSpace', + 'enum.php', + ]; + + yield [ + 'OptimizedDirectory\\UppercaseEnum', + 'OptimizedDirectory\\UppercaseEnum', + 'enum.php', + ]; } /** * @dataProvider dataClass - * @param string $className - * @param string $file */ public function testClass(string $className, string $expectedClassName, string $file): void { $factory = self::getContainer()->getByType(OptimizedDirectorySourceLocatorFactory::class); $locator = $factory->createByDirectory(__DIR__ . '/data/directory'); - $classReflector = new ClassReflector($locator); - $classReflection = $classReflector->reflect($className); + $reflector = new DefaultReflector($locator); + $classReflection = $reflector->reflectClass($className); $this->assertSame($expectedClassName, $classReflection->getName()); $this->assertNotNull($classReflection->getFileName()); $this->assertSame($file, basename($classReflection->getFileName())); @@ -67,28 +89,33 @@ public function dataFunctionExists(): array 'a.php', ], [ - 'doBar', - 'doBar', + 'OptimizedDirectory\\doBar', + 'OptimizedDirectory\\doBar', + 'b.php', + ], + [ + 'OptimizedDirectory\\doBaz', + 'OptimizedDirectory\\doBaz', 'b.php', ], [ - 'doBaz', - 'doBaz', + 'OptimizedDirectory\\dobaz', + 'OptimizedDirectory\\doBaz', 'b.php', ], [ - 'dobaz', - 'doBaz', + 'OptimizedDirectory\\get_smarty', + 'OptimizedDirectory\\get_smarty', 'b.php', ], [ - 'get_smarty', - 'get_smarty', + 'OptimizedDirectory\\get_smarty2', + 'OptimizedDirectory\\get_smarty2', 'b.php', ], [ - 'get_smarty2', - 'get_smarty2', + 'OptimizedDirectory\\upperCaseFunction', + 'OptimizedDirectory\\upperCaseFunction', 'b.php', ], ]; @@ -96,17 +123,13 @@ public function dataFunctionExists(): array /** * @dataProvider dataFunctionExists - * @param string $functionName - * @param string $expectedFunctionName - * @param string $file */ public function testFunctionExists(string $functionName, string $expectedFunctionName, string $file): void { $factory = self::getContainer()->getByType(OptimizedDirectorySourceLocatorFactory::class); $locator = $factory->createByDirectory(__DIR__ . '/data/directory'); - $classReflector = new ClassReflector($locator); - $functionReflector = new FunctionReflector($locator, $classReflector); - $functionReflection = $functionReflector->reflect($functionName); + $reflector = new DefaultReflector($locator); + $functionReflection = $reflector->reflectFunction($functionName); $this->assertSame($expectedFunctionName, $functionReflection->getName()); $this->assertNotNull($functionReflection->getFileName()); $this->assertSame($file, basename($functionReflection->getFileName())); @@ -122,17 +145,32 @@ public function dataFunctionDoesNotExist(): array /** * @dataProvider dataFunctionDoesNotExist - * @param string $functionName */ public function testFunctionDoesNotExist(string $functionName): void { $factory = self::getContainer()->getByType(OptimizedDirectorySourceLocatorFactory::class); $locator = $factory->createByDirectory(__DIR__ . '/data/directory'); - $classReflector = new ClassReflector($locator); - $functionReflector = new FunctionReflector($locator, $classReflector); + $reflector = new DefaultReflector($locator); $this->expectException(IdentifierNotFound::class); - $functionReflector->reflect($functionName); + $reflector->reflectFunction($functionName); + } + + public function testBug5525(): void + { + if (PHP_VERSION_ID < 70300) { + self::markTestSkipped('This test needs at least PHP 7.3 because of different PCRE engine'); + } + + $factory = self::getContainer()->getByType(OptimizedDirectorySourceLocatorFactory::class); + $locator = $factory->createByFiles([__DIR__ . '/data/bug-5525.php']); + $reflector = new DefaultReflector($locator); + + $class = $reflector->reflectClass('Faker\\Provider\\nl_BE\\Text'); + + /** @var string $className */ + $className = $class->getName(); + $this->assertSame('Faker\\Provider\\nl_BE\\Text', $className); } } diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php index 38185ccfc8..363bb4ee23 100644 --- a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/OptimizedSingleFileSourceLocatorTest.php @@ -2,19 +2,21 @@ namespace PHPStan\Reflection\BetterReflection\SourceLocator; -use PHPStan\BetterReflection\Reflector\ClassReflector; -use PHPStan\BetterReflection\Reflector\ConstantReflector; +use PHPStan\BetterReflection\Reflector\DefaultReflector; use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; -use PHPStan\BetterReflection\Reflector\FunctionReflector; -use PHPStan\Testing\TestCase; +use PHPStan\Testing\PHPStanTestCase; +use SingleFileSourceLocatorTestClass; +use stdClass; use TestSingleFileSourceLocator\AFoo; +use function str_replace; +use const PHP_VERSION_ID; -class OptimizedSingleFileSourceLocatorTest extends TestCase +class OptimizedSingleFileSourceLocatorTest extends PHPStanTestCase { - public function dataClass(): array + public function dataClass(): iterable { - return [ + yield from [ [ AFoo::class, AFoo::class, @@ -26,30 +28,37 @@ public function dataClass(): array __DIR__ . '/data/a.php', ], [ - \SingleFileSourceLocatorTestClass::class, - \SingleFileSourceLocatorTestClass::class, + SingleFileSourceLocatorTestClass::class, + SingleFileSourceLocatorTestClass::class, __DIR__ . '/data/b.php', ], [ 'SinglefilesourceLocatortestClass', - \SingleFileSourceLocatorTestClass::class, + SingleFileSourceLocatorTestClass::class, __DIR__ . '/data/b.php', ], ]; + + if (PHP_VERSION_ID < 80100) { + return; + } + + yield [ + 'OptimizedDirectory\\TestEnum', + 'OptimizedDirectory\\TestEnum', + __DIR__ . '/data/directory/enum.php', + ]; } /** * @dataProvider dataClass - * @param string $className - * @param string $expectedClassName - * @param string $file */ public function testClass(string $className, string $expectedClassName, string $file): void { $factory = self::getContainer()->getByType(OptimizedSingleFileSourceLocatorFactory::class); $locator = $factory->create($file); - $classReflector = new ClassReflector($locator); - $classReflection = $classReflector->reflect($className); + $reflector = new DefaultReflector($locator); + $classReflection = $reflector->reflectClass($className); $this->assertSame($expectedClassName, $classReflection->getName()); } @@ -81,17 +90,13 @@ public function dataFunction(): array /** * @dataProvider dataFunction - * @param string $functionName - * @param string $expectedFunctionName - * @param string $file */ public function testFunction(string $functionName, string $expectedFunctionName, string $file): void { $factory = self::getContainer()->getByType(OptimizedSingleFileSourceLocatorFactory::class); $locator = $factory->create($file); - $classReflector = new ClassReflector($locator); - $functionReflector = new FunctionReflector($locator, $classReflector); - $functionReflection = $functionReflector->reflect($functionName); + $reflector = new DefaultReflector($locator); + $functionReflection = $reflector->reflectFunction($functionName); $this->assertSame($expectedFunctionName, $functionReflection->getName()); } @@ -114,23 +119,25 @@ public function dataConst(): array 'const_with_dir_const', str_replace('\\', '/', __DIR__ . '/data'), ], + [ + 'OPTIMIZED_SFSL_OBJECT_CONSTANT', + new stdClass(), + ], ]; } /** * @dataProvider dataConst - * @param string $constantName * @param mixed $value */ public function testConst(string $constantName, $value): void { $factory = self::getContainer()->getByType(OptimizedSingleFileSourceLocatorFactory::class); $locator = $factory->create(__DIR__ . '/data/const.php'); - $classReflector = new ClassReflector($locator); - $constantReflector = new ConstantReflector($locator, $classReflector); - $constant = $constantReflector->reflect($constantName); + $reflector = new DefaultReflector($locator); + $constant = $reflector->reflectConstant($constantName); $this->assertSame($constantName, $constant->getName()); - $this->assertSame($value, $constant->getValue()); + $this->assertEquals($value, $constant->getValue()); } public function dataConstUnknown(): array @@ -142,16 +149,14 @@ public function dataConstUnknown(): array /** * @dataProvider dataConstUnknown - * @param string $constantName */ public function testConstUnknown(string $constantName): void { $factory = self::getContainer()->getByType(OptimizedSingleFileSourceLocatorFactory::class); $locator = $factory->create(__DIR__ . '/data/const.php'); - $classReflector = new ClassReflector($locator); - $constantReflector = new ConstantReflector($locator, $classReflector); + $reflector = new DefaultReflector($locator); $this->expectException(IdentifierNotFound::class); - $constantReflector->reflect($constantName); + $reflector->reflectConstant($constantName); } } diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/bug-5525.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/bug-5525.php new file mode 100644 index 0000000000..7bc1aeb4b9 --- /dev/null +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/bug-5525.php @@ -0,0 +1,25348 @@ += 8.1 + +namespace OptimizedDirectory; + +enum TestEnum: int +{ + + case ONE = 1; + +} + +enum BackedByStringWithoutSpace:string +{ + // cases +} + +Enum UppercaseEnum:string +{ + +} diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceStubber/Php8StubsSourceStubberTest.php b/tests/PHPStan/Reflection/BetterReflection/SourceStubber/Php8StubsSourceStubberTest.php index 8cc9f4fb49..c0d4d9164e 100644 --- a/tests/PHPStan/Reflection/BetterReflection/SourceStubber/Php8StubsSourceStubberTest.php +++ b/tests/PHPStan/Reflection/BetterReflection/SourceStubber/Php8StubsSourceStubberTest.php @@ -4,54 +4,41 @@ use PhpParser\Lexer\Emulative; use PhpParser\ParserFactory; -use PHPStan\BetterReflection\Reflector\ClassReflector; -use PHPStan\BetterReflection\Reflector\FunctionReflector; +use PHPStan\BetterReflection\Reflector\DefaultReflector; +use PHPStan\BetterReflection\Reflector\Reflector; use PHPStan\BetterReflection\SourceLocator\Ast\Locator; use PHPStan\BetterReflection\SourceLocator\Type\PhpInternalSourceLocator; use PHPUnit\Framework\TestCase; +use Throwable; class Php8StubsSourceStubberTest extends TestCase { public function testClass(): void { - /** @var ClassReflector $classReflector */ - [$classReflector] = $this->getReflectors(); - $reflection = $classReflector->reflect(\Throwable::class); - $this->assertSame(\Throwable::class, $reflection->getName()); + $reflection = $this->getReflector()->reflectClass(Throwable::class); + $this->assertSame(Throwable::class, $reflection->getName()); } public function testFunction(): void { - /** @var FunctionReflector $functionReflector */ - [, $functionReflector] = $this->getReflectors(); - $reflection = $functionReflector->reflect('htmlspecialchars'); + $reflection = $this->getReflector()->reflectFunction('htmlspecialchars'); $this->assertSame('htmlspecialchars', $reflection->getName()); } - /** - * @return array{ClassReflector, FunctionReflector} - */ - private function getReflectors(): array + private function getReflector(): Reflector { // memoizing parser screws things up so we need to create the universe from the start $parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7, new Emulative([ 'usedAttributes' => ['comments', 'startLine', 'endLine', 'startFilePos', 'endFilePos'], ])); - /** @var FunctionReflector $functionReflector */ - $functionReflector = null; - $astLocator = new Locator($parser, static function () use (&$functionReflector): FunctionReflector { - return $functionReflector; - }); + $astLocator = new Locator($parser); $sourceStubber = new Php8StubsSourceStubber(); $phpInternalSourceLocator = new PhpInternalSourceLocator( $astLocator, - $sourceStubber + $sourceStubber, ); - $classReflector = new ClassReflector($phpInternalSourceLocator); - $functionReflector = new FunctionReflector($phpInternalSourceLocator, $classReflector); - - return [$classReflector, $functionReflector]; + return new DefaultReflector($phpInternalSourceLocator); } } diff --git a/tests/PHPStan/Reflection/ClassReflectionTest.php b/tests/PHPStan/Reflection/ClassReflectionTest.php index 86a016db66..2b7eaf367b 100644 --- a/tests/PHPStan/Reflection/ClassReflectionTest.php +++ b/tests/PHPStan/Reflection/ClassReflectionTest.php @@ -2,82 +2,113 @@ namespace PHPStan\Reflection; +use Attribute; use Attributes\IsAttribute; use Attributes\IsAttribute2; use Attributes\IsAttribute3; use Attributes\IsNotAttribute; +use GenericInheritance\C; +use HasTraitUse\Bar; +use HasTraitUse\Baz; +use HasTraitUse\Foo; +use HasTraitUse\FooTrait; +use HierarchyDistances\ExtendedIpsumInterface; +use HierarchyDistances\FirstIpsumInterface; +use HierarchyDistances\FirstLoremInterface; +use HierarchyDistances\Ipsum; +use HierarchyDistances\Lorem; +use HierarchyDistances\SecondIpsumInterface; +use HierarchyDistances\SecondLoremInterface; +use HierarchyDistances\ThirdIpsumInterface; +use HierarchyDistances\TraitOne; +use HierarchyDistances\TraitThree; +use HierarchyDistances\TraitTwo; +use NestedTraits\BarTrait; +use NestedTraits\BazChild; +use NestedTraits\BazTrait; +use NestedTraits\NoTrait; use PHPStan\Broker\Broker; use PHPStan\Php\PhpVersion; +use PHPStan\PhpDoc\PhpDocInheritanceResolver; +use PHPStan\PhpDoc\StubPhpDocProvider; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\FileTypeMapper; +use PHPStan\Type\IntegerType; +use ReflectionClass; +use ReflectionEnum; use WrongClassConstantFile\SecuredRouter; +use function array_map; +use function array_values; +use const PHP_VERSION_ID; -class ClassReflectionTest extends \PHPStan\Testing\TestCase +class ClassReflectionTest extends PHPStanTestCase { public function dataHasTraitUse(): array { return [ - [\HasTraitUse\Foo::class, true], - [\HasTraitUse\Bar::class, true], - [\HasTraitUse\Baz::class, false], + [Foo::class, true], + [Bar::class, true], + [Baz::class, false], ]; } /** * @dataProvider dataHasTraitUse * @param class-string $className - * @param bool $has */ public function testHasTraitUse(string $className, bool $has): void { $broker = $this->createMock(Broker::class); $fileTypeMapper = $this->createMock(FileTypeMapper::class); - $classReflection = new ClassReflection($broker, $fileTypeMapper, new PhpVersion(PHP_VERSION_ID), [], [], $className, new \ReflectionClass($className), null, null, null); - $this->assertSame($has, $classReflection->hasTraitUse(\HasTraitUse\FooTrait::class)); + $stubPhpDocProvider = $this->createMock(StubPhpDocProvider::class); + $phpDocInheritanceResolver = $this->createMock(PhpDocInheritanceResolver::class); + $classReflection = new ClassReflection($broker, $fileTypeMapper, $stubPhpDocProvider, $phpDocInheritanceResolver, new PhpVersion(PHP_VERSION_ID), [], [], $className, new ReflectionClass($className), null, null, null); + $this->assertSame($has, $classReflection->hasTraitUse(FooTrait::class)); } public function dataClassHierarchyDistances(): array { return [ [ - \HierarchyDistances\Lorem::class, + Lorem::class, [ - \HierarchyDistances\Lorem::class => 0, - \HierarchyDistances\TraitTwo::class => 1, - \HierarchyDistances\TraitThree::class => 2, - \HierarchyDistances\FirstLoremInterface::class => 3, - \HierarchyDistances\SecondLoremInterface::class => 4, + Lorem::class => 0, + TraitTwo::class => 1, + TraitThree::class => 2, + FirstLoremInterface::class => 3, + SecondLoremInterface::class => 4, ], ], [ - \HierarchyDistances\Ipsum::class, + Ipsum::class, PHP_VERSION_ID < 70400 ? [ - \HierarchyDistances\Ipsum::class => 0, - \HierarchyDistances\TraitOne::class => 1, - \HierarchyDistances\Lorem::class => 2, - \HierarchyDistances\TraitTwo::class => 3, - \HierarchyDistances\TraitThree::class => 4, - \HierarchyDistances\SecondLoremInterface::class => 5, - \HierarchyDistances\FirstLoremInterface::class => 6, - \HierarchyDistances\FirstIpsumInterface::class => 7, - \HierarchyDistances\ExtendedIpsumInterface::class => 8, - \HierarchyDistances\SecondIpsumInterface::class => 9, - \HierarchyDistances\ThirdIpsumInterface::class => 10, + Ipsum::class => 0, + TraitOne::class => 1, + Lorem::class => 2, + TraitTwo::class => 3, + TraitThree::class => 4, + SecondLoremInterface::class => 5, + FirstLoremInterface::class => 6, + FirstIpsumInterface::class => 7, + ExtendedIpsumInterface::class => 8, + SecondIpsumInterface::class => 9, + ThirdIpsumInterface::class => 10, ] : [ - \HierarchyDistances\Ipsum::class => 0, - \HierarchyDistances\TraitOne::class => 1, - \HierarchyDistances\Lorem::class => 2, - \HierarchyDistances\TraitTwo::class => 3, - \HierarchyDistances\TraitThree::class => 4, - \HierarchyDistances\FirstLoremInterface::class => 5, - \HierarchyDistances\SecondLoremInterface::class => 6, - \HierarchyDistances\FirstIpsumInterface::class => 7, - \HierarchyDistances\SecondIpsumInterface::class => 8, - \HierarchyDistances\ThirdIpsumInterface::class => 9, - \HierarchyDistances\ExtendedIpsumInterface::class => 10, + Ipsum::class => 0, + TraitOne::class => 1, + Lorem::class => 2, + TraitTwo::class => 3, + TraitThree::class => 4, + FirstLoremInterface::class => 5, + SecondLoremInterface::class => 6, + FirstIpsumInterface::class => 7, + SecondIpsumInterface::class => 8, + ThirdIpsumInterface::class => 9, + ExtendedIpsumInterface::class => 10, ], ], ]; @@ -90,35 +121,38 @@ public function dataClassHierarchyDistances(): array */ public function testClassHierarchyDistances( string $class, - array $expectedDistances + array $expectedDistances, ): void { $broker = $this->createReflectionProvider(); $fileTypeMapper = $this->createMock(FileTypeMapper::class); + $stubPhpDocProvider = $this->createMock(StubPhpDocProvider::class); + $phpDocInheritanceResolver = $this->createMock(PhpDocInheritanceResolver::class); $classReflection = new ClassReflection( $broker, $fileTypeMapper, + $stubPhpDocProvider, + $phpDocInheritanceResolver, new PhpVersion(PHP_VERSION_ID), [], [], $class, - new \ReflectionClass($class), + new ReflectionClass($class), + null, null, null, - null ); $this->assertSame( $expectedDistances, - $classReflection->getClassHierarchyDistances() + $classReflection->getClassHierarchyDistances(), ); } public function testVariadicTraitMethod(): void { - /** @var Broker $broker */ - $broker = self::getContainer()->getService('broker'); - $fooReflection = $broker->getClass(\HasTraitUse\Foo::class); + $reflectionProvider = $this->createReflectionProvider(); + $fooReflection = $reflectionProvider->getClass(Foo::class); $variadicMethod = $fooReflection->getNativeMethod('variadicMethod'); $methodVariant = ParametersAcceptorSelector::selectSingle($variadicMethod->getVariants()); $this->assertTrue($methodVariant->isVariadic()); @@ -126,46 +160,27 @@ public function testVariadicTraitMethod(): void public function testGenericInheritance(): void { - /** @var Broker $broker */ - $broker = self::getContainer()->getService('broker'); - $reflection = $broker->getClass(\GenericInheritance\C::class); + $reflectionProvider = $this->createReflectionProvider(); + $reflection = $reflectionProvider->getClass(C::class); $this->assertSame('GenericInheritance\\C', $reflection->getDisplayName()); $parent = $reflection->getParentClass(); - $this->assertNotFalse($parent); + $this->assertNotNull($parent); $this->assertSame('GenericInheritance\\C0', $parent->getDisplayName()); $this->assertSame([ - 'GenericInheritance\\I0', - 'GenericInheritance\\I1', 'GenericInheritance\\I', - ], array_map(static function (ClassReflection $r): string { - return $r->getDisplayName(); - }, array_values($reflection->getInterfaces()))); - } - - public function testGenericInheritanceOverride(): void - { - /** @var Broker $broker */ - $broker = self::getContainer()->getService('broker'); - $reflection = $broker->getClass(\GenericInheritance\Override::class); - - $this->assertSame([ - 'GenericInheritance\\I0', + 'GenericInheritance\\I0', 'GenericInheritance\\I1', - 'GenericInheritance\\I', - ], array_map(static function (ClassReflection $r): string { - return $r->getDisplayName(); - }, array_values($reflection->getInterfaces()))); + ], array_map(static fn (ClassReflection $r): string => $r->getDisplayName(), array_values($reflection->getInterfaces()))); } public function testIsGenericWithStubPhpDoc(): void { - /** @var Broker $broker */ - $broker = self::getContainer()->getService('broker'); - $reflection = $broker->getClass(\ReflectionClass::class); + $reflectionProvider = $this->createReflectionProvider(); + $reflection = $reflectionProvider->getClass(ReflectionClass::class); $this->assertTrue($reflection->isGeneric()); } @@ -183,27 +198,25 @@ public function dataIsAttributeClass(): array [ IsAttribute2::class, true, - \Attribute::IS_REPEATABLE, + Attribute::IS_REPEATABLE, ], [ IsAttribute3::class, true, - \Attribute::IS_REPEATABLE | \Attribute::TARGET_PROPERTY, + Attribute::IS_REPEATABLE | Attribute::TARGET_PROPERTY, ], ]; } /** * @dataProvider dataIsAttributeClass - * @param string $className - * @param bool $expected */ - public function testIsAttributeClass(string $className, bool $expected, int $expectedFlags = \Attribute::TARGET_ALL): void + public function testIsAttributeClass(string $className, bool $expected, int $expectedFlags = Attribute::TARGET_ALL): void { if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { $this->markTestSkipped('Test requires PHP 8.0.'); } - $reflectionProvider = $this->createBroker(); + $reflectionProvider = $this->createReflectionProvider(); $reflection = $reflectionProvider->getClass($className); $this->assertSame($expected, $reflection->isAttributeClass()); if (!$expected) { @@ -214,7 +227,7 @@ public function testIsAttributeClass(string $className, bool $expected, int $exp public function testDeprecatedConstantFromAnotherFile(): void { - $reflectionProvider = $this->createBroker(); + $reflectionProvider = $this->createReflectionProvider(); $reflection = $reflectionProvider->getClass(SecuredRouter::class); $constant = $reflection->getConstant('SECURED'); $this->assertTrue($constant->isDeprecated()->yes()); @@ -224,20 +237,17 @@ public function testDeprecatedConstantFromAnotherFile(): void * @dataProvider dataNestedRecursiveTraits * @param class-string $className * @param array $expected - * @param bool $recursive */ public function testGetTraits(string $className, array $expected, bool $recursive): void { - $reflectionProvider = $this->createBroker(); + $reflectionProvider = $this->createReflectionProvider(); $this->assertSame( array_map( - static function (ClassReflection $classReflection): string { - return $classReflection->getNativeReflection()->getName(); - }, - $reflectionProvider->getClass($className)->getTraits($recursive) + static fn (ClassReflection $classReflection): string => $classReflection->getNativeReflection()->getName(), + $reflectionProvider->getClass($className)->getTraits($recursive), ), - $expected + $expected, ); } @@ -245,12 +255,12 @@ public function dataNestedRecursiveTraits(): array { return [ [ - \NestedTraits\NoTrait::class, + NoTrait::class, [], false, ], [ - \NestedTraits\NoTrait::class, + NoTrait::class, [], true, ], @@ -271,14 +281,14 @@ public function dataNestedRecursiveTraits(): array [ \NestedTraits\Bar::class, [ - \NestedTraits\BarTrait::class => \NestedTraits\BarTrait::class, + BarTrait::class => BarTrait::class, ], false, ], [ \NestedTraits\Bar::class, [ - \NestedTraits\BarTrait::class => \NestedTraits\BarTrait::class, + BarTrait::class => BarTrait::class, \NestedTraits\FooTrait::class => \NestedTraits\FooTrait::class, ], true, @@ -286,29 +296,29 @@ public function dataNestedRecursiveTraits(): array [ \NestedTraits\Baz::class, [ - \NestedTraits\BazTrait::class => \NestedTraits\BazTrait::class, + BazTrait::class => BazTrait::class, ], false, ], [ \NestedTraits\Baz::class, [ - \NestedTraits\BazTrait::class => \NestedTraits\BazTrait::class, - \NestedTraits\BarTrait::class => \NestedTraits\BarTrait::class, + BazTrait::class => BazTrait::class, + BarTrait::class => BarTrait::class, \NestedTraits\FooTrait::class => \NestedTraits\FooTrait::class, ], true, ], [ - \NestedTraits\BazChild::class, + BazChild::class, [], false, ], [ - \NestedTraits\BazChild::class, + BazChild::class, [ - \NestedTraits\BazTrait::class => \NestedTraits\BazTrait::class, - \NestedTraits\BarTrait::class => \NestedTraits\BarTrait::class, + BazTrait::class => BazTrait::class, + BarTrait::class => BarTrait::class, \NestedTraits\FooTrait::class => \NestedTraits\FooTrait::class, ], true, @@ -316,4 +326,29 @@ public function dataNestedRecursiveTraits(): array ]; } + public function testEnumIsFinal(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $reflectionProvider = $this->createReflectionProvider(); + $enum = $reflectionProvider->getClass('PHPStan\Fixture\TestEnum'); + $this->assertTrue($enum->isEnum()); + $this->assertInstanceOf(ReflectionEnum::class, $enum->getNativeReflection()); + $this->assertTrue($enum->isFinal()); + $this->assertTrue($enum->isFinalByKeyword()); + } + + public function testBackedEnumType(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $reflectionProvider = $this->createReflectionProvider(); + $enum = $reflectionProvider->getClass('PHPStan\Fixture\TestEnum'); + $this->assertInstanceOf(IntegerType::class, $enum->getBackedEnumType()); + } + } diff --git a/tests/PHPStan/Reflection/GenericParametersAcceptorResolverTest.php b/tests/PHPStan/Reflection/GenericParametersAcceptorResolverTest.php index 909057cfb5..f75e021960 100644 --- a/tests/PHPStan/Reflection/GenericParametersAcceptorResolverTest.php +++ b/tests/PHPStan/Reflection/GenericParametersAcceptorResolverTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Reflection; use PHPStan\Reflection\Php\DummyParameter; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\TemplateTypeFactory; @@ -17,8 +18,11 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function count; +use function get_class; +use function sprintf; -class GenericParametersAcceptorResolverTest extends \PHPStan\Testing\TestCase +class GenericParametersAcceptorResolverTest extends PHPStanTestCase { /** @@ -26,14 +30,12 @@ class GenericParametersAcceptorResolverTest extends \PHPStan\Testing\TestCase */ public function dataResolve(): array { - $templateType = static function (string $name, ?Type $type = null): Type { - return TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - $name, - $type, - TemplateTypeVariance::createInvariant() - ); - }; + $templateType = static fn (string $name, ?Type $type = null): Type => TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + $name, + $type, + TemplateTypeVariance::createInvariant(), + ); return [ 'one param, one arg' => [ @@ -52,11 +54,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), ], false, - new NullType() + new NullType(), ), new FunctionVariant( new TemplateTypeMap([ @@ -70,11 +72,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), ], false, - new NullType() + new NullType(), ), ], 'two params, two args, return type' => [ @@ -95,7 +97,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -103,11 +105,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), ], false, - $templateType('U') + $templateType('U'), ), new FunctionVariant( new TemplateTypeMap([ @@ -122,7 +124,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -130,11 +132,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), ], false, - new IntegerType() + new IntegerType(), ), ], 'mixed types' => [ @@ -154,7 +156,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -162,11 +164,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), ], false, - $templateType('T') + $templateType('T'), ), new FunctionVariant( new TemplateTypeMap([ @@ -183,7 +185,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -194,14 +196,14 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), ], false, new UnionType([ new ObjectType('DateTime'), new IntegerType(), - ]) + ]), ), ], 'parameter default value' => [ @@ -221,7 +223,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -229,11 +231,11 @@ public function dataResolve(): array true, PassedByReference::createNo(), false, - new IntegerType() + new IntegerType(), ), ], false, - new NullType() + new NullType(), ), new FunctionVariant( new TemplateTypeMap([ @@ -248,7 +250,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -256,11 +258,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), ], false, - new NullType() + new NullType(), ), ], 'variadic parameter' => [ @@ -283,7 +285,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -291,11 +293,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), true, - null + null, ), ], true, - $templateType('U') + $templateType('U'), ), new FunctionVariant( new TemplateTypeMap([ @@ -310,7 +312,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -318,11 +320,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), true, - null + null, ), ], false, - new IntegerType() + new IntegerType(), ), ], 'missing args' => [ @@ -342,7 +344,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -350,11 +352,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), ], false, - new NullType() + new NullType(), ), new FunctionVariant( new TemplateTypeMap([ @@ -369,7 +371,7 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), new DummyParameter( 'b', @@ -377,11 +379,11 @@ public function dataResolve(): array false, PassedByReference::createNo(), false, - null + null, ), ], false, - new NullType() + new NullType(), ), ], 'constant string arg resolved to constant string' => [ @@ -397,7 +399,7 @@ public function dataResolve(): array new DummyParameter('str', $templateType('T'), false, null, false, null), ], false, - $templateType('T') + $templateType('T'), ), new FunctionVariant( TemplateTypeMap::createEmpty(), @@ -406,7 +408,7 @@ public function dataResolve(): array new DummyParameter('str', new StringType(), false, null, false, null), ], false, - new StringType() + new StringType(), ), ], ]; @@ -414,24 +416,24 @@ public function dataResolve(): array /** * @dataProvider dataResolve - * @param \PHPStan\Type\Type[] $argTypes + * @param Type[] $argTypes */ public function testResolve(array $argTypes, ParametersAcceptor $parametersAcceptor, ParametersAcceptor $expectedResult): void { $result = GenericParametersAcceptorResolver::resolve( $argTypes, - $parametersAcceptor + $parametersAcceptor, ); $this->assertInstanceOf( get_class($expectedResult->getReturnType()), $result->getReturnType(), - 'Unexpected return type' + 'Unexpected return type', ); $this->assertSame( $expectedResult->getReturnType()->describe(VerbosityLevel::precise()), $result->getReturnType()->describe(VerbosityLevel::precise()), - 'Unexpected return type' + 'Unexpected return type', ); $resultParameters = $result->getParameters(); @@ -443,12 +445,12 @@ public function testResolve(array $argTypes, ParametersAcceptor $parametersAccep $this->assertInstanceOf( get_class($param->getType()), $resultParameters[$i]->getType(), - sprintf('Unexpected parameter %d', $i + 1) + sprintf('Unexpected parameter %d', $i + 1), ); $this->assertSame( $param->getType()->describe(VerbosityLevel::precise()), $resultParameters[$i]->getType()->describe(VerbosityLevel::precise()), - sprintf('Unexpected parameter %d', $i + 1) + sprintf('Unexpected parameter %d', $i + 1), ); } } diff --git a/tests/PHPStan/Reflection/MixedTypeTest.php b/tests/PHPStan/Reflection/MixedTypeTest.php index edc0370428..76991c57a9 100644 --- a/tests/PHPStan/Reflection/MixedTypeTest.php +++ b/tests/PHPStan/Reflection/MixedTypeTest.php @@ -4,10 +4,11 @@ use NativeMixedType\Foo; use PhpParser\Node\Name; -use PHPStan\Testing\TestCase; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\MixedType; +use const PHP_VERSION_ID; -class MixedTypeTest extends TestCase +class MixedTypeTest extends PHPStanTestCase { public function testMixedType(): void @@ -16,7 +17,7 @@ public function testMixedType(): void $this->markTestSkipped('Test requires PHP 8.0'); } - $reflectionProvider = $this->createBroker(); + $reflectionProvider = $this->createReflectionProvider(); $class = $reflectionProvider->getClass(Foo::class); $propertyType = $class->getNativeProperty('fooProp')->getNativeType(); $this->assertInstanceOf(MixedType::class, $propertyType); diff --git a/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php b/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php index ecf59410e3..0703fd4902 100644 --- a/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php +++ b/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php @@ -2,9 +2,13 @@ namespace PHPStan\Reflection; +use DateInterval; +use DateTimeInterface; +use Generator; use PhpParser\Node\Name; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\Php\DummyParameter; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -19,18 +23,20 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\ResourceType; use PHPStan\Type\StringType; +use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function count; -class ParametersAcceptorSelectorTest extends \PHPStan\Testing\TestCase +class ParametersAcceptorSelectorTest extends PHPStanTestCase { - public function dataSelectFromTypes(): \Generator + public function dataSelectFromTypes(): Generator { require_once __DIR__ . '/data/function-definitions.php'; - $broker = $this->createBroker(); + $reflectionProvider = $this->createReflectionProvider(); - $arrayRandVariants = $broker->getFunction(new Name('array_rand'), null)->getVariants(); + $arrayRandVariants = $reflectionProvider->getFunction(new Name('array_rand'), null)->getVariants(); yield [ [ new ArrayType(new MixedType(), new MixedType()), @@ -50,11 +56,11 @@ public function dataSelectFromTypes(): \Generator $arrayRandVariants[1], ]; - $datePeriodConstructorVariants = $broker->getClass('DatePeriod')->getNativeMethod('__construct')->getVariants(); + $datePeriodConstructorVariants = $reflectionProvider->getClass('DatePeriod')->getNativeMethod('__construct')->getVariants(); yield [ [ - new ObjectType(\DateTimeInterface::class), - new ObjectType(\DateInterval::class), + new ObjectType(DateTimeInterface::class), + new ObjectType(DateInterval::class), new IntegerType(), new IntegerType(), ], @@ -64,9 +70,9 @@ public function dataSelectFromTypes(): \Generator ]; yield [ [ - new ObjectType(\DateTimeInterface::class), - new ObjectType(\DateInterval::class), - new ObjectType(\DateTimeInterface::class), + new ObjectType(DateTimeInterface::class), + new ObjectType(DateInterval::class), + new ObjectType(DateTimeInterface::class), new IntegerType(), ], $datePeriodConstructorVariants, @@ -83,7 +89,7 @@ public function dataSelectFromTypes(): \Generator $datePeriodConstructorVariants[2], ]; - $ibaseWaitEventVariants = $broker->getFunction(new Name('ibase_wait_event'), null)->getVariants(); + $ibaseWaitEventVariants = $reflectionProvider->getFunction(new Name('ibase_wait_event'), null)->getVariants(); yield [ [ new ResourceType(), @@ -120,7 +126,7 @@ public function dataSelectFromTypes(): \Generator new MixedType(), PassedByReference::createNo(), false, - null + null, ), new NativeParameterReflection( 'event|args', @@ -128,15 +134,15 @@ public function dataSelectFromTypes(): \Generator new MixedType(), PassedByReference::createNo(), true, - null + null, ), ], true, - new StringType() + new StringType(), ), ]; - $absVariants = $broker->getFunction(new Name('abs'), null)->getVariants(); + $absVariants = $reflectionProvider->getFunction(new Name('abs'), null)->getVariants(); yield [ [ new FloatType(), @@ -165,7 +171,7 @@ public function dataSelectFromTypes(): \Generator $absVariants[2], ]; - $strtokVariants = $broker->getFunction(new Name('strtok'), null)->getVariants(); + $strtokVariants = $reflectionProvider->getFunction(new Name('strtok'), null)->getVariants(); yield [ [], $strtokVariants, @@ -180,7 +186,7 @@ public function dataSelectFromTypes(): \Generator new StringType(), PassedByReference::createNo(), false, - null + null, ), new NativeParameterReflection( 'token', @@ -188,11 +194,11 @@ public function dataSelectFromTypes(): \Generator new StringType(), PassedByReference::createNo(), false, - null + null, ), ], false, - new UnionType([new StringType(), new ConstantBooleanType(false)]) + new UnionType([new StringType(), new ConstantBooleanType(false)]), ), ]; yield [ @@ -215,7 +221,7 @@ public function dataSelectFromTypes(): \Generator new IntegerType(), PassedByReference::createNo(), false, - null + null, ), new NativeParameterReflection( 'intVariadic', @@ -223,11 +229,11 @@ public function dataSelectFromTypes(): \Generator new IntegerType(), PassedByReference::createNo(), true, - null + null, ), ], true, - new IntegerType() + new IntegerType(), ), new FunctionVariant( TemplateTypeMap::createEmpty(), @@ -239,7 +245,7 @@ public function dataSelectFromTypes(): \Generator new IntegerType(), PassedByReference::createNo(), false, - null + null, ), new NativeParameterReflection( 'floatVariadic', @@ -247,11 +253,11 @@ public function dataSelectFromTypes(): \Generator new FloatType(), PassedByReference::createNo(), true, - null + null, ), ], true, - new IntegerType() + new IntegerType(), ), ]; @@ -284,11 +290,11 @@ public function dataSelectFromTypes(): \Generator false, PassedByReference::createNo(), false, - new ConstantIntegerType(1) + new ConstantIntegerType(1), ), ], false, - new NullType() + new NullType(), ), new FunctionVariant( TemplateTypeMap::createEmpty(), @@ -300,11 +306,11 @@ public function dataSelectFromTypes(): \Generator false, PassedByReference::createNo(), false, - new ConstantIntegerType(2) + new ConstantIntegerType(2), ), ], false, - new NullType() + new NullType(), ), ]; @@ -327,11 +333,11 @@ public function dataSelectFromTypes(): \Generator new UnionType([ new ConstantIntegerType(1), new ConstantIntegerType(2), - ]) + ]), ), ], false, - new NullType() + new NullType(), ), ]; @@ -346,11 +352,11 @@ public function dataSelectFromTypes(): \Generator false, PassedByReference::createNo(), false, - new ConstantIntegerType(1) + new ConstantIntegerType(1), ), ], false, - new NullType() + new NullType(), ), new FunctionVariant( TemplateTypeMap::createEmpty(), @@ -362,11 +368,11 @@ public function dataSelectFromTypes(): \Generator false, PassedByReference::createNo(), false, - null + null, ), ], false, - new NullType() + new NullType(), ), ]; @@ -386,11 +392,11 @@ public function dataSelectFromTypes(): \Generator false, PassedByReference::createNo(), false, - null + null, ), ], false, - new NullType() + new NullType(), ), ]; @@ -405,16 +411,16 @@ public function dataSelectFromTypes(): \Generator TemplateTypeScope::createWithFunction('a'), 'T', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), false, PassedByReference::createNo(), false, - null + null, ), ], false, - new NullType() + new NullType(), ), ]; @@ -434,27 +440,25 @@ public function dataSelectFromTypes(): \Generator false, PassedByReference::createNo(), false, - null + null, ), ], false, - new NullType() + new NullType(), ), ]; } /** * @dataProvider dataSelectFromTypes - * @param \PHPStan\Type\Type[] $types + * @param Type[] $types * @param ParametersAcceptor[] $variants - * @param bool $unpack - * @param ParametersAcceptor $expected */ public function testSelectFromTypes( array $types, array $variants, bool $unpack, - ParametersAcceptor $expected + ParametersAcceptor $expected, ): void { $selectedAcceptor = ParametersAcceptorSelector::selectFromTypes($types, $variants, $unpack); @@ -463,36 +467,36 @@ public function testSelectFromTypes( $expectedParameter = $expected->getParameters()[$i]; $this->assertSame( $expectedParameter->getName(), - $parameter->getName() + $parameter->getName(), ); $this->assertSame( $expectedParameter->isOptional(), - $parameter->isOptional() + $parameter->isOptional(), ); $this->assertSame( $expectedParameter->getType()->describe(VerbosityLevel::precise()), - $parameter->getType()->describe(VerbosityLevel::precise()) + $parameter->getType()->describe(VerbosityLevel::precise()), ); $this->assertTrue( - $expectedParameter->passedByReference()->equals($parameter->passedByReference()) + $expectedParameter->passedByReference()->equals($parameter->passedByReference()), ); $this->assertSame( $expectedParameter->isVariadic(), - $parameter->isVariadic() + $parameter->isVariadic(), ); if ($expectedParameter->getDefaultValue() === null) { $this->assertNull($parameter->getDefaultValue()); } else { $this->assertSame( $expectedParameter->getDefaultValue()->describe(VerbosityLevel::precise()), - $parameter->getDefaultValue() !== null ? $parameter->getDefaultValue()->describe(VerbosityLevel::precise()) : null + $parameter->getDefaultValue() !== null ? $parameter->getDefaultValue()->describe(VerbosityLevel::precise()) : null, ); } } $this->assertSame( $expected->getReturnType()->describe(VerbosityLevel::precise()), - $selectedAcceptor->getReturnType()->describe(VerbosityLevel::precise()) + $selectedAcceptor->getReturnType()->describe(VerbosityLevel::precise()), ); $this->assertSame($expected->isVariadic(), $selectedAcceptor->isVariadic()); } diff --git a/tests/PHPStan/Reflection/Php/UniversalObjectCratesClassReflectionExtensionTest.php b/tests/PHPStan/Reflection/Php/UniversalObjectCratesClassReflectionExtensionTest.php index 0c5c5d812b..da08f803b7 100644 --- a/tests/PHPStan/Reflection/Php/UniversalObjectCratesClassReflectionExtensionTest.php +++ b/tests/PHPStan/Reflection/Php/UniversalObjectCratesClassReflectionExtensionTest.php @@ -2,45 +2,44 @@ namespace PHPStan\Reflection\Php; -use PHPStan\Broker\Broker; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\ObjectType; use PHPStan\Type\StringType; +use stdClass; -class UniversalObjectCratesClassReflectionExtensionTest extends \PHPStan\Testing\TestCase +class UniversalObjectCratesClassReflectionExtensionTest extends PHPStanTestCase { public function testNonexistentClass(): void { - $broker = self::getContainer()->getByType(Broker::class); - $extension = new UniversalObjectCratesClassReflectionExtension([ + $reflectionProvider = $this->createReflectionProvider(); + $extension = new UniversalObjectCratesClassReflectionExtension($reflectionProvider, [ 'NonexistentClass', 'stdClass', ]); - $extension->setBroker($broker); - $this->assertTrue($extension->hasProperty($broker->getClass(\stdClass::class), 'foo')); + $this->assertTrue($extension->hasProperty($reflectionProvider->getClass(stdClass::class), 'foo')); } public function testDifferentGetSetType(): void { require_once __DIR__ . '/data/universal-object-crates.php'; - $broker = self::getContainer()->getByType(Broker::class); - $extension = new UniversalObjectCratesClassReflectionExtension([ + $reflectionProvider = $this->createReflectionProvider(); + $extension = new UniversalObjectCratesClassReflectionExtension($reflectionProvider, [ 'UniversalObjectCreates\DifferentGetSetTypes', ]); - $extension->setBroker($broker); $this->assertEquals( new ObjectType('UniversalObjectCreates\DifferentGetSetTypesValue'), $extension - ->getProperty($broker->getClass('UniversalObjectCreates\DifferentGetSetTypes'), 'foo') - ->getReadableType() + ->getProperty($reflectionProvider->getClass('UniversalObjectCreates\DifferentGetSetTypes'), 'foo') + ->getReadableType(), ); $this->assertEquals( new StringType(), $extension - ->getProperty($broker->getClass('UniversalObjectCreates\DifferentGetSetTypes'), 'foo') - ->getWritableType() + ->getProperty($reflectionProvider->getClass('UniversalObjectCreates\DifferentGetSetTypes'), 'foo') + ->getWritableType(), ); } diff --git a/tests/PHPStan/Reflection/ReflectionProviderTest.php b/tests/PHPStan/Reflection/ReflectionProviderTest.php index 431214c141..d88cac6925 100644 --- a/tests/PHPStan/Reflection/ReflectionProviderTest.php +++ b/tests/PHPStan/Reflection/ReflectionProviderTest.php @@ -2,13 +2,16 @@ namespace PHPStan\Reflection; +use DateTime; use PhpParser\Node\Name; -use PHPStan\Testing\TestCase; +use PHPStan\Testing\PHPStanTestCase; +use PHPStan\TrinaryLogic; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use const PHP_VERSION_ID; -class ReflectionProviderTest extends TestCase +class ReflectionProviderTest extends PHPStanTestCase { public function dataFunctionThrowType(): iterable @@ -25,10 +28,17 @@ public function dataFunctionThrowType(): iterable ]; } - yield [ - 'bcdiv', - new ObjectType('DivisionByZeroError'), - ]; + if (PHP_VERSION_ID >= 80000) { + yield [ + 'bcdiv', + new ObjectType('DivisionByZeroError'), + ]; + } else { + yield [ + 'bcdiv', + null, + ]; + } yield [ 'GEOSRelateMatch', @@ -43,8 +53,6 @@ public function dataFunctionThrowType(): iterable /** * @dataProvider dataFunctionThrowType - * @param string $functionName - * @param ?Type $expectedThrowType */ public function testFunctionThrowType(string $functionName, ?Type $expectedThrowType): void { @@ -58,20 +66,56 @@ public function testFunctionThrowType(string $functionName, ?Type $expectedThrow $this->assertNotNull($throwType); $this->assertSame( $expectedThrowType->describe(VerbosityLevel::precise()), - $throwType->describe(VerbosityLevel::precise()) + $throwType->describe(VerbosityLevel::precise()), ); } + public function dataFunctionDeprecated(): iterable + { + if (PHP_VERSION_ID < 80000) { + yield 'create_function' => [ + 'create_function', + PHP_VERSION_ID >= 70200, + ]; + yield 'each' => [ + 'each', + PHP_VERSION_ID >= 70200, + ]; + } + + if (PHP_VERSION_ID < 90000) { + yield 'date_sunrise' => [ + 'date_sunrise', + PHP_VERSION_ID >= 80100, + ]; + } + + yield 'strtolower' => [ + 'strtolower', + false, + ]; + } + + /** + * @dataProvider dataFunctionDeprecated + */ + public function testFunctionDeprecated(string $functionName, bool $isDeprecated): void + { + $reflectionProvider = $this->createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name($functionName), null); + $this->assertEquals(TrinaryLogic::createFromBoolean($isDeprecated), $function->isDeprecated()); + } + public function dataMethodThrowType(): array { return [ [ - \DateTime::class, + DateTime::class, '__construct', new ObjectType('Exception'), ], [ - \DateTime::class, + DateTime::class, 'format', null, ], @@ -80,13 +124,10 @@ public function dataMethodThrowType(): array /** * @dataProvider dataMethodThrowType - * @param string $className - * @param string $methodName - * @param ?Type $expectedThrowType */ public function testMethodThrowType(string $className, string $methodName, ?Type $expectedThrowType): void { - $reflectionProvider = $this->createBroker(); + $reflectionProvider = $this->createReflectionProvider(); $class = $reflectionProvider->getClass($className); $method = $class->getNativeMethod($methodName); $throwType = $method->getThrowType(); @@ -97,7 +138,7 @@ public function testMethodThrowType(string $className, string $methodName, ?Type $this->assertNotNull($throwType); $this->assertSame( $expectedThrowType->describe(VerbosityLevel::precise()), - $throwType->describe(VerbosityLevel::precise()) + $throwType->describe(VerbosityLevel::precise()), ); } diff --git a/tests/PHPStan/Reflection/SignatureMap/FunctionMetadataTest.php b/tests/PHPStan/Reflection/SignatureMap/FunctionMetadataTest.php index ac5feca510..24ef8431ee 100644 --- a/tests/PHPStan/Reflection/SignatureMap/FunctionMetadataTest.php +++ b/tests/PHPStan/Reflection/SignatureMap/FunctionMetadataTest.php @@ -4,9 +4,9 @@ use Nette\Schema\Expect; use Nette\Schema\Processor; -use PHPStan\Testing\TestCase; +use PHPStan\Testing\PHPStanTestCase; -class FunctionMetadataTest extends TestCase +class FunctionMetadataTest extends PHPStanTestCase { public function testSchema(): void @@ -18,7 +18,7 @@ public function testSchema(): void $processor->process(Expect::arrayOf( Expect::structure([ 'hasSideEffects' => Expect::bool()->required(), - ])->required() + ])->required(), )->required(), $data); } diff --git a/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php b/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php index db00ba439a..1fa92f8ee7 100644 --- a/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php +++ b/tests/PHPStan/Reflection/SignatureMap/Php8SignatureMapProviderTest.php @@ -7,7 +7,7 @@ use PHPStan\Reflection\BetterReflection\SourceLocator\FileNodesFetcher; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\PassedByReference; -use PHPStan\Testing\TestCase; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\CallableType; @@ -25,8 +25,11 @@ use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use PHPStan\Type\VoidType; +use function array_map; +use function array_merge; +use function count; -class Php8SignatureMapProviderTest extends TestCase +class Php8SignatureMapProviderTest extends PHPStanTestCase { public function dataFunctions(): array @@ -105,8 +108,8 @@ public function dataFunctions(): array [ 'name' => 'array', 'optional' => false, - 'type' => new ArrayType(new MixedType(), new MixedType()), - 'nativeType' => new ArrayType(new MixedType(), new MixedType()), + 'type' => new UnionType([new ArrayType(new MixedType(), new MixedType()), new ObjectWithoutClassType()]), + 'nativeType' => new UnionType([new ArrayType(new MixedType(), new MixedType()), new ObjectWithoutClassType()]), 'passedByReference' => PassedByReference::createReadsArgument(), 'variadic' => false, ], @@ -127,7 +130,7 @@ public function testFunctions( array $parameters, Type $returnType, Type $nativeReturnType, - bool $variadic + bool $variadic, ): void { $provider = $this->createProvider(); @@ -140,10 +143,10 @@ private function createProvider(): Php8SignatureMapProvider return new Php8SignatureMapProvider( new FunctionSignatureMapProvider( self::getContainer()->getByType(SignatureMapParser::class), - new PhpVersion(80000) + new PhpVersion(80000), ), self::getContainer()->getByType(FileNodesFetcher::class), - self::getContainer()->getByType(FileTypeMapper::class) + self::getContainer()->getByType(FileTypeMapper::class), ); } @@ -220,7 +223,7 @@ public function dataMethods(): array 'uasort', [ [ - 'name' => 'cmp_function', + 'name' => 'callback', 'optional' => false, 'type' => new CallableType([ new NativeParameterReflection('', false, new MixedType(true), PassedByReference::createNo(), false, null), @@ -248,7 +251,7 @@ public function testMethods( array $parameters, Type $returnType, Type $nativeReturnType, - bool $variadic + bool $variadic, ): void { $provider = $this->createProvider(); @@ -258,17 +261,13 @@ public function testMethods( /** * @param mixed[] $expectedParameters - * @param Type $expectedReturnType - * @param Type $expectedNativeReturnType - * @param bool $expectedVariadic - * @param FunctionSignature $actualSignature */ private function assertSignature( array $expectedParameters, Type $expectedReturnType, Type $expectedNativeReturnType, bool $expectedVariadic, - FunctionSignature $actualSignature + FunctionSignature $actualSignature, ): void { $this->assertCount(count($expectedParameters), $actualSignature->getParameters()); @@ -289,14 +288,11 @@ private function assertSignature( public function dataParseAll(): array { - return array_map(static function (string $file): array { - return [__DIR__ . '/../../../../vendor/phpstan/php-8-stubs/' . $file]; - }, array_merge(Php8StubsMap::CLASSES, Php8StubsMap::FUNCTIONS)); + return array_map(static fn (string $file): array => [__DIR__ . '/../../../../vendor/phpstan/php-8-stubs/' . $file], array_merge(Php8StubsMap::CLASSES, Php8StubsMap::FUNCTIONS)); } /** * @dataProvider dataParseAll - * @param string $stubFile */ public function testParseAll(string $stubFile): void { diff --git a/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php b/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php index a0b85a89ce..0f00bd56f2 100644 --- a/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php +++ b/tests/PHPStan/Reflection/SignatureMap/SignatureMapParserTest.php @@ -2,8 +2,12 @@ namespace PHPStan\Reflection\SignatureMap; +use DateInterval; +use DateTime; use PHPStan\Php\PhpVersion; +use PHPStan\PhpDocParser\Parser\ParserException; use PHPStan\Reflection\PassedByReference; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\CallableType; @@ -17,12 +21,20 @@ use PHPStan\Type\StringType; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use ReflectionParameter; +use Throwable; +use function array_keys; +use function count; +use function explode; +use function sprintf; +use function strpos; -class SignatureMapParserTest extends \PHPStan\Testing\TestCase +class SignatureMapParserTest extends PHPStanTestCase { public function dataGetFunctions(): array { + $reflectionProvider = $this->createReflectionProvider(); return [ [ ['int', 'fp' => 'resource', 'fields' => 'array', 'delimiter=' => 'string', 'enclosure=' => 'string', 'escape_char=' => 'string'], @@ -35,7 +47,7 @@ public function dataGetFunctions(): array new ResourceType(), new MixedType(), PassedByReference::createNo(), - false + false, ), new ParameterSignature( 'fields', @@ -43,7 +55,7 @@ public function dataGetFunctions(): array new ArrayType(new MixedType(), new MixedType()), new MixedType(), PassedByReference::createNo(), - false + false, ), new ParameterSignature( 'delimiter', @@ -51,7 +63,7 @@ public function dataGetFunctions(): array new StringType(), new MixedType(), PassedByReference::createNo(), - false + false, ), new ParameterSignature( 'enclosure', @@ -59,7 +71,7 @@ public function dataGetFunctions(): array new StringType(), new MixedType(), PassedByReference::createNo(), - false + false, ), new ParameterSignature( 'escape_char', @@ -67,12 +79,12 @@ public function dataGetFunctions(): array new StringType(), new MixedType(), PassedByReference::createNo(), - false + false, ), ], new IntegerType(), new MixedType(), - false + false, ), ], [ @@ -86,12 +98,12 @@ public function dataGetFunctions(): array new ResourceType(), new MixedType(), PassedByReference::createNo(), - false + false, ), ], new BooleanType(), new MixedType(), - false + false, ), ], [ @@ -105,12 +117,12 @@ public function dataGetFunctions(): array new ArrayType(new MixedType(), new MixedType()), new MixedType(), PassedByReference::createReadsArgument(), - false + false, ), ], new BooleanType(), new MixedType(), - false + false, ), ], [ @@ -127,7 +139,7 @@ public function dataGetFunctions(): array ]), new MixedType(), PassedByReference::createNo(), - false + false, ), new ParameterSignature( 'out', @@ -135,7 +147,7 @@ public function dataGetFunctions(): array new StringType(), new MixedType(), PassedByReference::createCreatesNewVariable(), - false + false, ), new ParameterSignature( 'notext', @@ -143,12 +155,12 @@ public function dataGetFunctions(): array new BooleanType(), new MixedType(), PassedByReference::createNo(), - false + false, ), ], new BooleanType(), new MixedType(), - false + false, ), ], [ @@ -157,12 +169,12 @@ public function dataGetFunctions(): array new FunctionSignature( [], new UnionType([ - new ObjectType(\Throwable::class), + new ObjectType(Throwable::class), new ObjectType('Foo'), new NullType(), ]), new MixedType(), - false + false, ), ], [ @@ -172,7 +184,7 @@ public function dataGetFunctions(): array [], new MixedType(), new MixedType(), - false + false, ), ], [ @@ -186,7 +198,7 @@ public function dataGetFunctions(): array new ArrayType(new MixedType(), new MixedType()), new MixedType(), PassedByReference::createNo(), - false + false, ), new ParameterSignature( 'arr2', @@ -194,7 +206,7 @@ public function dataGetFunctions(): array new ArrayType(new MixedType(), new MixedType()), new MixedType(), PassedByReference::createNo(), - false + false, ), new ParameterSignature( '...', @@ -202,12 +214,12 @@ public function dataGetFunctions(): array new ArrayType(new MixedType(), new MixedType()), new MixedType(), PassedByReference::createNo(), - true + true, ), ], new ArrayType(new MixedType(), new MixedType()), new MixedType(), - true + true, ), ], [ @@ -221,7 +233,7 @@ public function dataGetFunctions(): array new CallableType(), new MixedType(), PassedByReference::createNo(), - false + false, ), new ParameterSignature( 'event', @@ -229,7 +241,7 @@ public function dataGetFunctions(): array new StringType(), new MixedType(), PassedByReference::createNo(), - false + false, ), new ParameterSignature( '...', @@ -237,12 +249,12 @@ public function dataGetFunctions(): array new MixedType(), new MixedType(), PassedByReference::createNo(), - true + true, ), ], new ResourceType(), new MixedType(), - true + true, ), ], [ @@ -256,7 +268,7 @@ public function dataGetFunctions(): array new StringType(), new MixedType(), PassedByReference::createNo(), - false + false, ), new ParameterSignature( 'args', @@ -264,12 +276,12 @@ public function dataGetFunctions(): array new MixedType(), new MixedType(), PassedByReference::createNo(), - true + true, ), ], new StringType(), new MixedType(), - true + true, ), ], [ @@ -283,7 +295,7 @@ public function dataGetFunctions(): array new StringType(), new MixedType(), PassedByReference::createNo(), - false + false, ), new ParameterSignature( 'args', @@ -291,12 +303,12 @@ public function dataGetFunctions(): array new MixedType(), new MixedType(), PassedByReference::createNo(), - true + true, ), ], new StringType(), new MixedType(), - true + true, ), ], [ @@ -304,28 +316,28 @@ public function dataGetFunctions(): array null, new FunctionSignature( [], - new ArrayType(new IntegerType(), new ObjectType(\ReflectionParameter::class)), + new ArrayType(new IntegerType(), new ObjectType(ReflectionParameter::class)), new MixedType(), - false + false, ), ], [ ['static', 'interval' => 'DateInterval'], - \DateTime::class, + DateTime::class, new FunctionSignature( [ new ParameterSignature( 'interval', false, - new ObjectType(\DateInterval::class), + new ObjectType(DateInterval::class), new MixedType(), PassedByReference::createNo(), - false + false, ), ], - new StaticType(\DateTime::class), + new StaticType($reflectionProvider->getClass(DateTime::class)), new MixedType(), - false + false, ), ], [ @@ -339,7 +351,7 @@ public function dataGetFunctions(): array new StringType(), new MixedType(), PassedByReference::createReadsArgument(), - false + false, ), new ParameterSignature( 'strings', @@ -347,12 +359,12 @@ public function dataGetFunctions(): array new StringType(), new MixedType(), PassedByReference::createReadsArgument(), - true + true, ), ], new BooleanType(), new MixedType(), - true + true, ), ], ]; @@ -361,13 +373,11 @@ public function dataGetFunctions(): array /** * @dataProvider dataGetFunctions * @param mixed[] $map - * @param string|null $className - * @param \PHPStan\Reflection\SignatureMap\FunctionSignature $expectedSignature */ public function testGetFunctions( array $map, ?string $className, - FunctionSignature $expectedSignature + FunctionSignature $expectedSignature, ): void { /** @var SignatureMapParser $parser */ @@ -376,7 +386,7 @@ public function testGetFunctions( $this->assertCount( count($expectedSignature->getParameters()), $functionSignature->getParameters(), - 'Number of parameters does not match.' + 'Number of parameters does not match.', ); foreach ($functionSignature->getParameters() as $i => $parameterSignature) { @@ -384,38 +394,38 @@ public function testGetFunctions( $this->assertSame( $expectedParameterSignature->getName(), $parameterSignature->getName(), - sprintf('Name of parameter #%d does not match.', $i) + sprintf('Name of parameter #%d does not match.', $i), ); $this->assertSame( $expectedParameterSignature->isOptional(), $parameterSignature->isOptional(), - sprintf('Optionality of parameter $%s does not match.', $parameterSignature->getName()) + sprintf('Optionality of parameter $%s does not match.', $parameterSignature->getName()), ); $this->assertSame( $expectedParameterSignature->getType()->describe(VerbosityLevel::precise()), $parameterSignature->getType()->describe(VerbosityLevel::precise()), - sprintf('Type of parameter $%s does not match.', $parameterSignature->getName()) + sprintf('Type of parameter $%s does not match.', $parameterSignature->getName()), ); $this->assertTrue( $expectedParameterSignature->passedByReference()->equals($parameterSignature->passedByReference()), - sprintf('Passed-by-reference of parameter $%s does not match.', $parameterSignature->getName()) + sprintf('Passed-by-reference of parameter $%s does not match.', $parameterSignature->getName()), ); $this->assertSame( $expectedParameterSignature->isVariadic(), $parameterSignature->isVariadic(), - sprintf('Variadicity of parameter $%s does not match.', $parameterSignature->getName()) + sprintf('Variadicity of parameter $%s does not match.', $parameterSignature->getName()), ); } $this->assertSame( $expectedSignature->getReturnType()->describe(VerbosityLevel::precise()), $functionSignature->getReturnType()->describe(VerbosityLevel::precise()), - 'Return type does not match.' + 'Return type does not match.', ); $this->assertSame( $expectedSignature->isVariadic(), $functionSignature->isVariadic(), - 'Variadicity does not match.' + 'Variadicity does not match.', ); } @@ -429,7 +439,6 @@ public function dataParseAll(): array /** * @dataProvider dataParseAll - * @param int $phpVersionId */ public function testParseAll(int $phpVersionId): void { @@ -448,7 +457,7 @@ public function testParseAll(int $phpVersionId): void try { $signature = $provider->getFunctionSignature($functionName, $className); $count++; - } catch (\PHPStan\PhpDocParser\Parser\ParserException $e) { + } catch (ParserException $e) { $this->fail(sprintf('Could not parse %s: %s.', $functionName, $e->getMessage())); } diff --git a/tests/PHPStan/Reflection/Type/IntersectionTypeMethodReflectionTest.php b/tests/PHPStan/Reflection/Type/IntersectionTypeMethodReflectionTest.php index cbd42d5f9c..f6c315b219 100644 --- a/tests/PHPStan/Reflection/Type/IntersectionTypeMethodReflectionTest.php +++ b/tests/PHPStan/Reflection/Type/IntersectionTypeMethodReflectionTest.php @@ -3,9 +3,10 @@ namespace PHPStan\Reflection\Type; use PHPStan\Reflection\MethodReflection; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; -class IntersectionTypeMethodReflectionTest extends \PHPStan\Testing\TestCase +class IntersectionTypeMethodReflectionTest extends PHPStanTestCase { public function testCollectsDeprecatedMessages(): void @@ -16,7 +17,7 @@ public function testCollectsDeprecatedMessages(): void $this->createDeprecatedMethod(TrinaryLogic::createYes(), 'Deprecated'), $this->createDeprecatedMethod(TrinaryLogic::createMaybe(), 'Maybe deprecated'), $this->createDeprecatedMethod(TrinaryLogic::createNo(), 'Not deprecated'), - ] + ], ); $this->assertSame('Deprecated', $reflection->getDeprecatedDescription()); @@ -29,7 +30,7 @@ public function testMultipleDeprecationsAreJoined(): void [ $this->createDeprecatedMethod(TrinaryLogic::createYes(), 'Deprecated #1'), $this->createDeprecatedMethod(TrinaryLogic::createYes(), 'Deprecated #2'), - ] + ], ); $this->assertSame('Deprecated #1 Deprecated #2', $reflection->getDeprecatedDescription()); diff --git a/tests/PHPStan/Reflection/Type/UnionTypeMethodReflectionTest.php b/tests/PHPStan/Reflection/Type/UnionTypeMethodReflectionTest.php index 93a168e43e..9df28a8e83 100644 --- a/tests/PHPStan/Reflection/Type/UnionTypeMethodReflectionTest.php +++ b/tests/PHPStan/Reflection/Type/UnionTypeMethodReflectionTest.php @@ -3,9 +3,10 @@ namespace PHPStan\Reflection\Type; use PHPStan\Reflection\MethodReflection; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; -class UnionTypeMethodReflectionTest extends \PHPStan\Testing\TestCase +class UnionTypeMethodReflectionTest extends PHPStanTestCase { public function testCollectsDeprecatedMessages(): void @@ -16,7 +17,7 @@ public function testCollectsDeprecatedMessages(): void $this->createDeprecatedMethod(TrinaryLogic::createYes(), 'Deprecated'), $this->createDeprecatedMethod(TrinaryLogic::createMaybe(), 'Maybe deprecated'), $this->createDeprecatedMethod(TrinaryLogic::createNo(), 'Not deprecated'), - ] + ], ); $this->assertSame('Deprecated', $reflection->getDeprecatedDescription()); @@ -29,7 +30,7 @@ public function testMultipleDeprecationsAreJoined(): void [ $this->createDeprecatedMethod(TrinaryLogic::createYes(), 'Deprecated #1'), $this->createDeprecatedMethod(TrinaryLogic::createYes(), 'Deprecated #2'), - ] + ], ); $this->assertSame('Deprecated #1 Deprecated #2', $reflection->getDeprecatedDescription()); diff --git a/tests/PHPStan/Reflection/UnionTypesTest.php b/tests/PHPStan/Reflection/UnionTypesTest.php index 692b4f31e1..ca150ccbe4 100644 --- a/tests/PHPStan/Reflection/UnionTypesTest.php +++ b/tests/PHPStan/Reflection/UnionTypesTest.php @@ -4,11 +4,12 @@ use NativeUnionTypes\Foo; use PhpParser\Node\Name; -use PHPStan\Testing\TestCase; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use const PHP_VERSION_ID; -class UnionTypesTest extends TestCase +class UnionTypesTest extends PHPStanTestCase { public function testUnionTypes(): void @@ -19,7 +20,7 @@ public function testUnionTypes(): void require_once __DIR__ . '/../../../stubs/runtime/ReflectionUnionType.php'; - $reflectionProvider = $this->createBroker(); + $reflectionProvider = $this->createReflectionProvider(); $class = $reflectionProvider->getClass(Foo::class); $propertyType = $class->getNativeProperty('fooProp')->getNativeType(); $this->assertInstanceOf(UnionType::class, $propertyType); diff --git a/tests/PHPStan/Reflection/data/GenericInheritance.php b/tests/PHPStan/Reflection/data/GenericInheritance.php index 9de88b7f3a..e378da01c6 100644 --- a/tests/PHPStan/Reflection/data/GenericInheritance.php +++ b/tests/PHPStan/Reflection/data/GenericInheritance.php @@ -46,10 +46,3 @@ class C0 implements I { */ class C extends C0 { } - - -/** - * @implements I<\DateTimeInterface> - */ -class Override extends C { -} diff --git a/tests/PHPStan/Rules/AlwaysFailRule.php b/tests/PHPStan/Rules/AlwaysFailRule.php index af3809d23f..d8df296181 100644 --- a/tests/PHPStan/Rules/AlwaysFailRule.php +++ b/tests/PHPStan/Rules/AlwaysFailRule.php @@ -6,9 +6,9 @@ use PHPStan\Analyser\Scope; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\FuncCall> + * @implements Rule */ -class AlwaysFailRule implements \PHPStan\Rules\Rule +class AlwaysFailRule implements Rule { public function getNodeType(): string diff --git a/tests/PHPStan/Rules/Api/ApiClassExtendsRuleTest.php b/tests/PHPStan/Rules/Api/ApiClassExtendsRuleTest.php index f85489374d..26651b4e97 100644 --- a/tests/PHPStan/Rules/Api/ApiClassExtendsRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiClassExtendsRuleTest.php @@ -2,7 +2,9 @@ namespace PHPStan\Rules\Api; +use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use function sprintf; /** * @extends RuleTestCase @@ -10,7 +12,7 @@ class ApiClassExtendsRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new ApiClassExtendsRule(new ApiRuleHelper(), $this->createReflectionProvider()); } @@ -24,7 +26,7 @@ public function testRuleOutOfPhpStan(): void { $tip = sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", - 'https://github.com/phpstan/phpstan/discussions' + 'https://github.com/phpstan/phpstan/discussions', ); $this->analyse([__DIR__ . '/data/class-extends-out-of-phpstan.php'], [ diff --git a/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php b/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php index abba2f945c..127fcd7877 100644 --- a/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php @@ -2,7 +2,9 @@ namespace PHPStan\Rules\Api; +use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use function sprintf; /** * @extends RuleTestCase @@ -10,7 +12,7 @@ class ApiClassImplementsRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new ApiClassImplementsRule(new ApiRuleHelper(), $this->createReflectionProvider()); } @@ -24,18 +26,18 @@ public function testRuleOutOfPhpStan(): void { $tip = sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", - 'https://github.com/phpstan/phpstan/discussions' + 'https://github.com/phpstan/phpstan/discussions', ); $this->analyse([__DIR__ . '/data/class-implements-out-of-phpstan.php'], [ [ 'Implementing PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 16, + 17, $tip, ], [ 'Implementing PHPStan\Type\Type is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 50, + 51, $tip, ], ]); diff --git a/tests/PHPStan/Rules/Api/ApiInstantiationRuleTest.php b/tests/PHPStan/Rules/Api/ApiInstantiationRuleTest.php index 27311eafd9..0d96c5a664 100644 --- a/tests/PHPStan/Rules/Api/ApiInstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiInstantiationRuleTest.php @@ -2,7 +2,9 @@ namespace PHPStan\Rules\Api; +use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use function sprintf; /** * @extends RuleTestCase @@ -10,11 +12,11 @@ class ApiInstantiationRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new ApiInstantiationRule( new ApiRuleHelper(), - $this->createReflectionProvider() + $this->createReflectionProvider(), ); } @@ -27,7 +29,7 @@ public function testRuleOutOfPhpStan(): void { $tip = sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", - 'https://github.com/phpstan/phpstan/discussions' + 'https://github.com/phpstan/phpstan/discussions', ); $this->analyse([__DIR__ . '/data/new-out-of-phpstan.php'], [ [ diff --git a/tests/PHPStan/Rules/Api/ApiInterfaceExtendsRuleTest.php b/tests/PHPStan/Rules/Api/ApiInterfaceExtendsRuleTest.php index b15ce60be8..db20d90013 100644 --- a/tests/PHPStan/Rules/Api/ApiInterfaceExtendsRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiInterfaceExtendsRuleTest.php @@ -2,7 +2,9 @@ namespace PHPStan\Rules\Api; +use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use function sprintf; /** * @extends RuleTestCase @@ -10,7 +12,7 @@ class ApiInterfaceExtendsRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new ApiInterfaceExtendsRule(new ApiRuleHelper(), $this->createReflectionProvider()); } @@ -24,7 +26,7 @@ public function testRuleOutOfPhpStan(): void { $tip = sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", - 'https://github.com/phpstan/phpstan/discussions' + 'https://github.com/phpstan/phpstan/discussions', ); $this->analyse([__DIR__ . '/data/interface-extends-out-of-phpstan.php'], [ diff --git a/tests/PHPStan/Rules/Api/ApiMethodCallRuleTest.php b/tests/PHPStan/Rules/Api/ApiMethodCallRuleTest.php index 1707090bca..d91f116610 100644 --- a/tests/PHPStan/Rules/Api/ApiMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiMethodCallRuleTest.php @@ -2,7 +2,9 @@ namespace PHPStan\Rules\Api; +use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use function sprintf; /** * @extends RuleTestCase @@ -10,7 +12,7 @@ class ApiMethodCallRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new ApiMethodCallRule(new ApiRuleHelper()); } @@ -24,7 +26,7 @@ public function testRuleOutOfPhpStan(): void { $tip = sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", - 'https://github.com/phpstan/phpstan/discussions' + 'https://github.com/phpstan/phpstan/discussions', ); $this->analyse([__DIR__ . '/data/method-call-out-of-phpstan.php'], [ diff --git a/tests/PHPStan/Rules/Api/ApiRuleHelperTest.php b/tests/PHPStan/Rules/Api/ApiRuleHelperTest.php index 4ff44e26b5..93d8428b77 100644 --- a/tests/PHPStan/Rules/Api/ApiRuleHelperTest.php +++ b/tests/PHPStan/Rules/Api/ApiRuleHelperTest.php @@ -141,7 +141,7 @@ public function testIsPhpStanCode( string $scopeFile, string $nameToCheck, ?string $declaringFileNameToCheck, - bool $expected + bool $expected, ): void { $rule = new ApiRuleHelper(); diff --git a/tests/PHPStan/Rules/Api/ApiStaticCallRuleTest.php b/tests/PHPStan/Rules/Api/ApiStaticCallRuleTest.php index 442eb0fb11..8331674e4b 100644 --- a/tests/PHPStan/Rules/Api/ApiStaticCallRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiStaticCallRuleTest.php @@ -2,7 +2,9 @@ namespace PHPStan\Rules\Api; +use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use function sprintf; /** * @extends RuleTestCase @@ -10,7 +12,7 @@ class ApiStaticCallRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new ApiStaticCallRule(new ApiRuleHelper(), $this->createReflectionProvider()); } @@ -24,7 +26,7 @@ public function testRuleOutOfPhpStan(): void { $tip = sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", - 'https://github.com/phpstan/phpstan/discussions' + 'https://github.com/phpstan/phpstan/discussions', ); $this->analyse([__DIR__ . '/data/static-call-out-of-phpstan.php'], [ diff --git a/tests/PHPStan/Rules/Api/ApiTraitUseRuleTest.php b/tests/PHPStan/Rules/Api/ApiTraitUseRuleTest.php index 96a2060067..da2dbbeefe 100644 --- a/tests/PHPStan/Rules/Api/ApiTraitUseRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiTraitUseRuleTest.php @@ -2,7 +2,9 @@ namespace PHPStan\Rules\Api; +use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use function sprintf; /** * @extends RuleTestCase @@ -10,7 +12,7 @@ class ApiTraitUseRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new ApiTraitUseRule(new ApiRuleHelper(), $this->createReflectionProvider()); } @@ -24,7 +26,7 @@ public function testRuleOutOfPhpStan(): void { $tip = sprintf( "If you think it should be covered by backward compatibility promise, open a discussion:\n %s\n\n See also:\n https://phpstan.org/developing-extensions/backward-compatibility-promise", - 'https://github.com/phpstan/phpstan/discussions' + 'https://github.com/phpstan/phpstan/discussions', ); $this->analyse([__DIR__ . '/data/trait-use-out-of-phpstan.php'], [ diff --git a/tests/PHPStan/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRuleTest.php b/tests/PHPStan/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRuleTest.php index 38db282fdc..4c50e26ccc 100644 --- a/tests/PHPStan/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRuleTest.php +++ b/tests/PHPStan/Rules/Api/PhpStanNamespaceIn3rdPartyPackageRuleTest.php @@ -4,7 +4,9 @@ use Nette\Utils\Json; use PHPStan\File\FileWriter; +use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use function unlink; /** * @extends RuleTestCase @@ -12,7 +14,7 @@ class PhpStanNamespaceIn3rdPartyPackageRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new PhpStanNamespaceIn3rdPartyPackageRule(new ApiRuleHelper()); } 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 ec185e0383..680bd40c57 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 @@ -8,6 +8,7 @@ use PHPStan\Reflection\ClassMemberAccessAnswerer; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionThrowTypeExtension; +use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\Generic\TemplateTypeReference; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Type; @@ -174,6 +175,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni // TODO: Implement setOffsetValueType() method. } + public function unsetOffset(Type $offsetType): Type + { + // TODO: Implement unsetOffset() method. + } + public function isCallable(): \PHPStan\TrinaryLogic { // TODO: Implement isCallable() method. @@ -229,11 +235,26 @@ public function isSmallerThanOrEqual(Type $otherType): \PHPStan\TrinaryLogic // TODO: Implement isSmallerThanOrEqual() method. } + public function isString(): \PHPStan\TrinaryLogic + { + // TODO: Implement isString() method. + } + public function isNumericString(): \PHPStan\TrinaryLogic { // TODO: Implement isNumericString() method. } + public function isNonEmptyString(): \PHPStan\TrinaryLogic + { + // TODO: Implement isNumericString() method. + } + + public function isLiteralString(): \PHPStan\TrinaryLogic + { + // TODO: Implement isLiteralString() method. + } + public function getSmallerType(): \PHPStan\Type\Type { // TODO: Implement getSmallerType() method. @@ -269,6 +290,16 @@ public function traverse(callable $cb): \PHPStan\Type\Type // TODO: Implement traverse() method. } + public function generalize(GeneralizePrecision $precision): Type + { + // TODO: Implement generalize() method. + } + + public function tryRemove(Type $typeToRemove): ?Type + { + // TODO: Implement tryRemove() method. + } + public static function __set_state(array $properties): \PHPStan\Type\Type { // TODO: Implement __set_state() method. diff --git a/tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php b/tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php index d6c27680be..75dcc16350 100644 --- a/tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php @@ -3,19 +3,21 @@ namespace PHPStan\Rules\Arrays; use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class AppendedArrayItemTypeRuleTest extends \PHPStan\Testing\RuleTestCase +class AppendedArrayItemTypeRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new AppendedArrayItemTypeRule( new PropertyReflectionFinder(), - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false) + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false), ); } @@ -29,15 +31,15 @@ public function testAppendedArrayItemType(): void 18, ], [ - 'Array (array) does not accept array(1, 2, 3).', + 'Array (array) does not accept array{1, 2, 3}.', 20, ], [ - 'Array (array) does not accept array(\'AppendedArrayItem\\\\Foo\', \'classMethod\').', + 'Array (array) does not accept array{\'AppendedArrayItem\\\\Foo\', \'classMethod\'}.', 23, ], [ - 'Array (array) does not accept array(\'Foo\', \'Hello world\').', + 'Array (array) does not accept array{\'Foo\', \'Hello world\'}.', 25, ], [ @@ -56,7 +58,7 @@ public function testAppendedArrayItemType(): void 'Array (array) does not accept AppendedArrayItem\Baz.', 79, ], - ] + ], ); } diff --git a/tests/PHPStan/Rules/Arrays/AppendedArrayKeyTypeRuleTest.php b/tests/PHPStan/Rules/Arrays/AppendedArrayKeyTypeRuleTest.php index 502df29bc4..d74a1ddd8e 100644 --- a/tests/PHPStan/Rules/Arrays/AppendedArrayKeyTypeRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/AppendedArrayKeyTypeRuleTest.php @@ -3,18 +3,20 @@ namespace PHPStan\Rules\Arrays; use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class AppendedArrayKeyTypeRuleTest extends \PHPStan\Testing\RuleTestCase +class AppendedArrayKeyTypeRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new AppendedArrayKeyTypeRule( new PropertyReflectionFinder(), - true + true, ); } @@ -45,7 +47,25 @@ public function testRule(): void 'Array (array) does not accept key 1.', 46, ], + [ + 'Array (array<1|2|3, string>) does not accept key int.', + 80, + ], + [ + 'Array (array<1|2|3, string>) does not accept key 4.', + 85, + ], ]); } + public function testBug5372Two(): void + { + $this->analyse([__DIR__ . '/data/bug-5372_2.php'], []); + } + + public function testBug5447(): void + { + $this->analyse([__DIR__ . '/data/bug-5447.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php b/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php index 195dbf6e94..5970a3e8f6 100644 --- a/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -18,7 +19,7 @@ protected function getRule(): Rule return new ArrayDestructuringRule( $ruleLevelHelper, - new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true) + new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true), ); } @@ -30,7 +31,7 @@ public function testRule(): void 11, ], [ - 'Offset 0 does not exist on array().', + 'Offset 0 does not exist on array{}.', 12, ], [ @@ -38,14 +39,28 @@ public function testRule(): void 13, ], [ - 'Offset 2 does not exist on array(1, 2).', + 'Offset 2 does not exist on array{1, 2}.', 15, ], [ - 'Offset \'a\' does not exist on array(\'b\' => 1).', + 'Offset \'a\' does not exist on array{b: 1}.', 22, ], ]); } + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/array-destructuring-nullsafe.php'], [ + [ + 'Cannot use array destructuring on array|null.', + 10, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php b/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php index d8b999e8e6..a570343822 100644 --- a/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php @@ -6,7 +6,7 @@ use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class DeadForeachRuleTest extends RuleTestCase { diff --git a/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php b/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php index 28e7898337..6877f37644 100644 --- a/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php @@ -2,16 +2,21 @@ namespace PHPStan\Rules\Arrays; +use PhpParser\PrettyPrinter\Standard; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use function define; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class DuplicateKeysInLiteralArraysRuleTest extends \PHPStan\Testing\RuleTestCase +class DuplicateKeysInLiteralArraysRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new DuplicateKeysInLiteralArraysRule( - new \PhpParser\PrettyPrinter\Standard() + new Standard(), ); } diff --git a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php index e1cc3685d0..625f884a61 100644 --- a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php @@ -2,13 +2,16 @@ namespace PHPStan\Rules\Arrays; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class InvalidKeyInArrayDimFetchRuleTest extends \PHPStan\Testing\RuleTestCase +class InvalidKeyInArrayDimFetchRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new InvalidKeyInArrayDimFetchRule(true); } diff --git a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php index 7bb10e9e47..62cf77d9f4 100644 --- a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php @@ -2,13 +2,16 @@ namespace PHPStan\Rules\Arrays; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class InvalidKeyInArrayItemRuleTest extends \PHPStan\Testing\RuleTestCase +class InvalidKeyInArrayItemRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new InvalidKeyInArrayItemRule(true); } diff --git a/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php b/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php index 6025adb242..e1feebb198 100644 --- a/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/IterableInForeachRuleTest.php @@ -2,17 +2,22 @@ namespace PHPStan\Rules\Arrays; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class IterableInForeachRuleTest extends \PHPStan\Testing\RuleTestCase +class IterableInForeachRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + private bool $checkExplicitMixed = false; + + protected function getRule(): Rule { - return new IterableInForeachRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false)); + return new IterableInForeachRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed)); } public function testCheckWithMaybes(): void @@ -34,4 +39,40 @@ public function testCheckWithMaybes(): void ]); } + public function testBug5744(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5744.php'], [ + /*[ + 'Argument of an invalid type mixed supplied for foreach, only iterables are supported.', + 15, + ],*/ + [ + 'Argument of an invalid type mixed supplied for foreach, only iterables are supported.', + 28, + ], + ]); + } + + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/foreach-iterable-nullsafe.php'], [ + [ + 'Argument of an invalid type array|null supplied for foreach, only iterables are supported.', + 14, + ], + ]); + } + + public function testBug6564(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6564.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 31fcb1c1fa..5621266e40 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -2,22 +2,27 @@ namespace PHPStan\Rules\Arrays; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class NonexistentOffsetInArrayDimFetchRuleTest extends \PHPStan\Testing\RuleTestCase +class NonexistentOffsetInArrayDimFetchRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + private bool $checkExplicitMixed = false; + + protected function getRule(): Rule { - $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false); + $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed); return new NonexistentOffsetInArrayDimFetchRule( $ruleLevelHelper, new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true), - true + true, ); } @@ -25,15 +30,15 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/nonexistent-offset.php'], [ [ - 'Offset \'b\' does not exist on array(\'a\' => stdClass, 0 => 2).', + 'Offset \'b\' does not exist on array{a: stdClass, 0: 2}.', 17, ], [ - 'Offset 1 does not exist on array(\'a\' => stdClass, 0 => 2).', + 'Offset 1 does not exist on array{a: stdClass, 0: 2}.', 18, ], [ - 'Offset \'a\' does not exist on array(\'b\' => 1).', + 'Offset \'a\' does not exist on array{b: 1}.', 55, ], [ @@ -79,23 +84,23 @@ public function testRule(): void 145, ], [ - 'Offset \'c\' does not exist on array(\'c\' => bool)|array(\'e\' => true).', + 'Offset \'c\' does not exist on array{c: bool}|array{e: true}.', 171, ], [ - 'Offset int does not exist on array()|array(1 => 1, 2 => 2)|array(3 => 3, 4 => 4).', + 'Offset int does not exist on array{}|array{1: 1, 2: 2}|array{3: 3, 4: 4}.', 190, ], [ - 'Offset int does not exist on array()|array(1 => 1, 2 => 2)|array(3 => 3, 4 => 4).', + 'Offset int does not exist on array{}|array{1: 1, 2: 2}|array{3: 3, 4: 4}.', 193, ], [ - 'Offset \'b\' does not exist on array(\'a\' => \'blabla\').', + 'Offset \'b\' does not exist on array{a: \'blabla\'}.', 225, ], [ - 'Offset \'b\' does not exist on array(\'a\' => \'blabla\').', + 'Offset \'b\' does not exist on array{a: \'blabla\'}.', 228, ], [ @@ -107,7 +112,7 @@ public function testRule(): void 253, ], [ - 'Cannot access offset \'a\' on array(\'a\' => 1, \'b\' => 1)|(Closure(): void).', + 'Cannot access offset \'a\' on array{a: 1, b: 1}|(Closure(): void).', 258, ], [ @@ -123,7 +128,7 @@ public function testRule(): void 312, ], [ - 'Offset \'baz\' does not exist on array(\'bar\' => 1, ?\'baz\' => 2).', + 'Offset \'baz\' does not exist on array{bar: 1, baz?: 2}.', 344, ], [ @@ -158,6 +163,10 @@ public function testRule(): void 'Cannot access offset \'foo\' on array|int.', 443, ], + [ + 'Offset \'feature_pretty…\' does not exist on array{version: non-empty-string, commit: string|null, pretty_version: string|null, feature_version: non-empty-string, feature_pretty_version?: string|null}.', + 504, + ], ]); } @@ -187,7 +196,7 @@ public function testAssignOp(): void { $this->analyse([__DIR__ . '/data/offset-access-assignop.php'], [ [ - 'Offset \'foo\' does not exist on array().', + 'Offset \'foo\' does not exist on array{}.', 4, ], [ @@ -278,4 +287,79 @@ public function testBug5169(): void ]); } + public function testBug3297(): void + { + $this->analyse([__DIR__ . '/data/bug-3297.php'], []); + } + + public function testBug4829(): void + { + $this->analyse([__DIR__ . '/data/bug-4829.php'], []); + } + + public function testBug3784(): void + { + $this->analyse([__DIR__ . '/data/bug-3784.php'], []); + } + + public function testBug3700(): void + { + $this->analyse([__DIR__ . '/data/bug-3700.php'], []); + } + + public function testBug4842(): void + { + $this->analyse([__DIR__ . '/data/bug-4842.php'], []); + } + + public function testBug5669(): void + { + $this->analyse([__DIR__ . '/data/bug-5669.php'], [ + [ + 'Access to offset \'%customer…\' on an unknown class Bug5669\arr.', + 26, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + + public function testBug5744(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5744.php'], [ + /*[ + 'Cannot access offset \'permission\' on mixed.', + 16, + ],*/ + [ + 'Cannot access offset \'permission\' on mixed.', + 29, + ], + ]); + } + + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/nonexistent-offset-nullsafe.php'], [ + [ + 'Offset 1 does not exist on array{a: int}.', + 18, + ], + ]); + } + + public function testBug4926(): void + { + $this->analyse([__DIR__ . '/data/bug-4926.php'], []); + } + + public function testBug3171(): void + { + $this->analyse([__DIR__ . '/data/bug-3171.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignOpRuleTest.php b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignOpRuleTest.php index 624b446d91..ab4a065cf7 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignOpRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignOpRuleTest.php @@ -4,15 +4,16 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class OffsetAccessAssignOpRuleTest extends \PHPStan\Testing\RuleTestCase +class OffsetAccessAssignOpRuleTest extends RuleTestCase { - /** @var bool */ - private $checkUnions; + private bool $checkUnions; protected function getRule(): Rule { @@ -37,4 +38,14 @@ public function testRuleWithoutUnions(): void $this->analyse([__DIR__ . '/data/offset-access-assignop.php'], []); } + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkUnions = true; + $this->analyse([__DIR__ . '/data/offset-access-assignop-nullsafe.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php index 88f38c8ee1..dff1942e3e 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessAssignmentRuleTest.php @@ -2,18 +2,20 @@ namespace PHPStan\Rules\Arrays; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class OffsetAccessAssignmentRuleTest extends \PHPStan\Testing\RuleTestCase +class OffsetAccessAssignmentRuleTest extends RuleTestCase { - /** @var bool */ - private $checkUnionTypes; + private bool $checkUnionTypes; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, $this->checkUnionTypes, false); return new OffsetAccessAssignmentRule($ruleLevelHelper); @@ -58,7 +60,7 @@ public function testOffsetAccessAssignmentToScalar(): void 68, ], [ - 'Cannot assign offset array(1, 2, 3) to SplObjectStorage.', + 'Cannot assign offset array{1, 2, 3} to SplObjectStorage.', 72, ], [ @@ -69,7 +71,7 @@ public function testOffsetAccessAssignmentToScalar(): void 'Cannot assign new offset to OffsetAccessAssignment\ObjectWithOffsetAccess.', 81, ], - ] + ], ); } @@ -100,7 +102,7 @@ public function testOffsetAccessAssignmentToScalarWithoutMaybes(): void 68, ], [ - 'Cannot assign offset array(1, 2, 3) to SplObjectStorage.', + 'Cannot assign offset array{1, 2, 3} to SplObjectStorage.', 72, ], [ @@ -111,7 +113,7 @@ public function testOffsetAccessAssignmentToScalarWithoutMaybes(): void 'Cannot assign new offset to OffsetAccessAssignment\ObjectWithOffsetAccess.', 81, ], - ] + ], ); } @@ -127,4 +129,20 @@ public function testAssignNewOffsetToStubbedClass(): void $this->analyse([__DIR__ . '/data/new-offset-stub.php'], []); } + + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/offset-access-assignment-nullsafe.php'], [ + [ + 'Cannot assign offset int|null to string.', + 14, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php b/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php index 366937c226..fdb32e4eb3 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessValueAssignmentRuleTest.php @@ -5,9 +5,10 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class OffsetAccessValueAssignmentRuleTest extends RuleTestCase { @@ -44,6 +45,24 @@ public function testRule(): void 'ArrayAccess does not accept float.', 38, ], + [ + 'ArrayAccess does not accept int.', + 58, + ], + ]); + } + + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/offset-access-value-assignment-nullsafe.php'], [ + [ + 'ArrayAccess does not accept int|null.', + 18, + ], ]); } diff --git a/tests/PHPStan/Rules/Arrays/OffsetAccessWithoutDimForReadingRuleTest.php b/tests/PHPStan/Rules/Arrays/OffsetAccessWithoutDimForReadingRuleTest.php index 027f8c420f..db6846cfa0 100644 --- a/tests/PHPStan/Rules/Arrays/OffsetAccessWithoutDimForReadingRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/OffsetAccessWithoutDimForReadingRuleTest.php @@ -2,13 +2,16 @@ namespace PHPStan\Rules\Arrays; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class OffsetAccessWithoutDimForReadingRuleTest extends \PHPStan\Testing\RuleTestCase +class OffsetAccessWithoutDimForReadingRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new OffsetAccessWithoutDimForReadingRule(); } @@ -82,7 +85,7 @@ public function testOffsetAccessWithoutDimForReading(): void 'Cannot use [] for reading.', 30, ], - ] + ], ); } diff --git a/tests/PHPStan/Rules/Arrays/UnpackIterableInArrayRuleTest.php b/tests/PHPStan/Rules/Arrays/UnpackIterableInArrayRuleTest.php index 833f4d6b78..66bacbcfec 100644 --- a/tests/PHPStan/Rules/Arrays/UnpackIterableInArrayRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/UnpackIterableInArrayRuleTest.php @@ -5,9 +5,10 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class UnpackIterableInArrayRuleTest extends RuleTestCase { @@ -38,4 +39,18 @@ public function testRule(): void ]); } + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/unpack-iterable-nullsafe.php'], [ + [ + 'Only iterables can be unpacked, array|null given.', + 17, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/appended-array-key.php b/tests/PHPStan/Rules/Arrays/data/appended-array-key.php index 3381fabab9..650e62e736 100644 --- a/tests/PHPStan/Rules/Arrays/data/appended-array-key.php +++ b/tests/PHPStan/Rules/Arrays/data/appended-array-key.php @@ -68,3 +68,21 @@ public function doBar() } } + +class MorePreciseKey +{ + + /** @var array<1|2|3, string> */ + private $test; + + public function doFoo(int $i): void + { + $this->test[$i] = 'foo'; + } + + public function doBar(): void + { + $this->test[4] = 'foo'; + } + +} diff --git a/tests/PHPStan/Rules/Arrays/data/array-destructuring-nullsafe.php b/tests/PHPStan/Rules/Arrays/data/array-destructuring-nullsafe.php new file mode 100644 index 0000000000..d8389afe5e --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/array-destructuring-nullsafe.php @@ -0,0 +1,23 @@ += 8.0 + +namespace ArrayDestructuringNullsafe; + +class Foo +{ + + public function doFooBar(?Bar $bar): void + { + [$a] = $bar?->getArray(); + } + +} + +class Bar +{ + + public function getArray(): array + { + return []; + } + +} diff --git a/tests/PHPStan/Rules/Arrays/data/array-destructuring.php b/tests/PHPStan/Rules/Arrays/data/array-destructuring.php index bfe0ff5421..8dd4b625e0 100644 --- a/tests/PHPStan/Rules/Arrays/data/array-destructuring.php +++ b/tests/PHPStan/Rules/Arrays/data/array-destructuring.php @@ -22,4 +22,33 @@ public function doBar(): void ['a' => $a] = ['b' => 1]; } + public function doBaz(): void + { + $arrayObject = new FooArrayObject(); + ['a' => $a] = $arrayObject; + } + +} + +class FooArrayObject implements \ArrayAccess +{ + + public function offsetGet($key) + { + return true; + } + + public function offsetSet($key, $value): void + { + } + + public function offsetUnset($key): void + { + } + + public function offsetExists($key): bool + { + return false; + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-3171.php b/tests/PHPStan/Rules/Arrays/data/bug-3171.php new file mode 100644 index 0000000000..96505bbf6c --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-3171.php @@ -0,0 +1,17 @@ + 'blah1', 'cat2' => 'blah2', ?'cat3' => 'blah3'). + public function stan() : void + { + $ret = []; + + // if $result has 1 element, the error does not occur + // if $result is [], the error occurs + $result = ["val1", "val2"]; + + foreach ($result as $val) { + // if I replace $val with a string, the error does not occur + //$val = "test"; + + // if I remove one assignment, the error does not occur + $ret[$val]['cat1'] = "blah1"; + $ret[$val]['cat2'] = "blah2"; + $ret[$val]['cat3'] = 'blah3'; + + $t1 = $ret[$val]['cat1']; + $t2 = $ret[$val]['cat2']; + $t3 = $ret[$val]['cat3']; // error occurs here + } + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-4842.php b/tests/PHPStan/Rules/Arrays/data/bug-4842.php new file mode 100644 index 0000000000..5230a347f9 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-4842.php @@ -0,0 +1,31 @@ +mappings = $mappings; + } + + /** + * @param "21021200"|"asd" $code + */ + function foo(string $code): string + { + if (isset($this->mappings[$code])) { + return (string)$this->mappings[$code]; + } + + throw new \RuntimeException(); + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-4926.php b/tests/PHPStan/Rules/Arrays/data/bug-4926.php new file mode 100644 index 0000000000..3e61b67f6b --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-4926.php @@ -0,0 +1,17 @@ +data['customer']['first_name'] ?? null; + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-5372_2.php b/tests/PHPStan/Rules/Arrays/data/bug-5372_2.php new file mode 100644 index 0000000000..5e6caebd42 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-5372_2.php @@ -0,0 +1,23 @@ + */ + private $map = []; + + /** + * @param array $values + */ + public function __construct(array $values) + { + assertType('array', $values); + foreach ($values as $v) { + assertType('non-empty-string', $v); + $this->map[$v] = 'whatever'; + } + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-5447.php b/tests/PHPStan/Rules/Arrays/data/bug-5447.php new file mode 100644 index 0000000000..d758be05b5 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-5447.php @@ -0,0 +1,36 @@ + + */ + private $parameters = []; + + /** + * @phpstan-param self::FIELD_* $key + */ + public function setParameter(string $key, string $value) : void + { + $this->parameters[$key] = $value; + } + + /** + * @phpstan-return array + */ + public function getParameters() : array + { + return $this->parameters; + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-5669.php b/tests/PHPStan/Rules/Arrays/data/bug-5669.php new file mode 100644 index 0000000000..a280cce420 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-5669.php @@ -0,0 +1,30 @@ + + */ + public function getReplacer() + { + return []; + } + +} + +class c extends a +{ + + public function getReplacer() + { + $replacer = parent::getReplacer(); + $replacer['%customer_salutation%'] = 'test'; + + return $replacer; + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-5744.php b/tests/PHPStan/Rules/Arrays/data/bug-5744.php new file mode 100644 index 0000000000..638911e179 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-5744.php @@ -0,0 +1,43 @@ + $commandData){ + var_dump($commandData["permission"]); + } + } + } + + /** + * @phpstan-param mixed[] $plugin + */ + public function sayHello2(array $plugin): void + { + if(isset($plugin["commands"])){ + $pluginCommands = $plugin["commands"]; + foreach($pluginCommands as $commandName => $commandData){ + var_dump($commandData["permission"]); + } + } + } + + public function sayHello3(array $plugin): void + { + if(isset($plugin["commands"]) and is_array($plugin["commands"])){ + $pluginCommands = $plugin["commands"]; + foreach($pluginCommands as $commandName => $commandData){ + var_dump($commandData["permission"]); + } + } + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-6564.php b/tests/PHPStan/Rules/Arrays/data/bug-6564.php new file mode 100644 index 0000000000..565310c9df --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-6564.php @@ -0,0 +1,20 @@ +isValueIterable()) { + /** @var mixed[] $value */ + foreach ($value as $_value) { + } + } + + + } + public function isValueIterable(): bool { + return true; + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/foreach-iterable-nullsafe.php b/tests/PHPStan/Rules/Arrays/data/foreach-iterable-nullsafe.php new file mode 100644 index 0000000000..4f19692219 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/foreach-iterable-nullsafe.php @@ -0,0 +1,17 @@ += 8.0 + +namespace IterablesInForeachNullsafe; + +class Foo +{ + + /** @var int[] */ + public array $array; +} + +function doFoo(?Foo $foo) +{ + foreach ($foo?->array as $x) { + // pass + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/nonexistent-offset-nullsafe.php b/tests/PHPStan/Rules/Arrays/data/nonexistent-offset-nullsafe.php new file mode 100644 index 0000000000..8185c01f8f --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/nonexistent-offset-nullsafe.php @@ -0,0 +1,19 @@ += 8.0 + +namespace NonexistentOffsetNullsafe; + +class Foo +{ + + /** @var array{a: int} */ + public array $array = [ + 'a' => 1, + ]; + +} + +function nonexistentOffsetOnArray(?Foo $foo): void +{ + echo $foo?->array['a']; + echo $foo?->array[1]; +} diff --git a/tests/PHPStan/Rules/Arrays/data/nonexistent-offset.php b/tests/PHPStan/Rules/Arrays/data/nonexistent-offset.php index db4335e930..065d636722 100644 --- a/tests/PHPStan/Rules/Arrays/data/nonexistent-offset.php +++ b/tests/PHPStan/Rules/Arrays/data/nonexistent-offset.php @@ -459,3 +459,52 @@ public function foo(array $array): int return 0; } } + +class MessageDescriptorTest +{ + + public function testDefinitions(): void + { + try { + doFoo(); + } catch (\TypeError $e) { + $trace = $e->getTrace(); + if (isset($trace[1]['args'][0])) { + $class = $trace[1]['args'][0]; + $this->fail(sprintf('Invalid phpDoc in class: %s', $class)); + } + + throw $e; + } + } + + /** @param array|null $array */ + function test($array): void { + var_dump($array['test1']['test2'] ?? true); + var_dump($array['test1'] ?? true); + } + +} + +/** + * @phpstan-type Version array{version: string, commit: string|null, pretty_version: string|null, feature_version?: string|null, feature_pretty_version?: string|null} + */ +class VersionGuesser +{ + /** + * @param array $versionData + * + * @phpstan-param Version $versionData + * + * @return array + * @phpstan-return Version + */ + private function postprocess(array $versionData): array + { + if (!empty($versionData['feature_version']) && $versionData['feature_version'] === $versionData['version'] && $versionData['feature_pretty_version'] === $versionData['pretty_version']) { + unset($versionData['feature_version'], $versionData['feature_pretty_version']); + } + + return $versionData; + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/offset-access-assignment-nullsafe.php b/tests/PHPStan/Rules/Arrays/data/offset-access-assignment-nullsafe.php new file mode 100644 index 0000000000..3ddac9f8c8 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/offset-access-assignment-nullsafe.php @@ -0,0 +1,15 @@ += 8.0 +declare(strict_types = 1); + +namespace OffsetAccessAssignmentNullsafe; + +class Bar +{ + public int $val; +} + +function doFoo(?Bar $bar) +{ + $str = 'abcd'; + $str[$bar?->val] = 'ok'; +} diff --git a/tests/PHPStan/Rules/Arrays/data/offset-access-assignment-to-scalar.php b/tests/PHPStan/Rules/Arrays/data/offset-access-assignment-to-scalar.php index 3818fd4ceb..a4723578fe 100644 --- a/tests/PHPStan/Rules/Arrays/data/offset-access-assignment-to-scalar.php +++ b/tests/PHPStan/Rules/Arrays/data/offset-access-assignment-to-scalar.php @@ -90,7 +90,7 @@ class ObjectWithOffsetAccess implements \ArrayAccess * @param string $offset * @return bool */ - public function offsetExists($offset) + public function offsetExists($offset): bool { return true; } @@ -99,6 +99,7 @@ public function offsetExists($offset) * @param string $offset * @return int */ + #[\ReturnTypeWillChange] public function offsetGet($offset) { return 0; @@ -109,7 +110,7 @@ public function offsetGet($offset) * @param int $value * @return void */ - public function offsetSet($offset, $value) + public function offsetSet($offset, $value): void { } @@ -117,7 +118,7 @@ public function offsetSet($offset, $value) * @param string $offset * @return void */ - public function offsetUnset($offset) + public function offsetUnset($offset): void { } diff --git a/tests/PHPStan/Rules/Arrays/data/offset-access-assignop-nullsafe.php b/tests/PHPStan/Rules/Arrays/data/offset-access-assignop-nullsafe.php new file mode 100644 index 0000000000..8b81b5d9aa --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/offset-access-assignop-nullsafe.php @@ -0,0 +1,22 @@ += 8.0 +declare(strict_types=1); + +namespace OffsetAccessAssignOpNullsafe; + +class Bar +{ + public const INDEX = 'b'; + + /** @phpstan-var Bar::INDEX */ + public string $index = self::INDEX; +} + +function doFoo(?Bar $bar) +{ + /** @var array $array */ + $array = [ + 'a' => 123, + ]; + + $array['b'] += 'str'; +} diff --git a/tests/PHPStan/Rules/Arrays/data/offset-access-value-assignment-nullsafe.php b/tests/PHPStan/Rules/Arrays/data/offset-access-value-assignment-nullsafe.php new file mode 100644 index 0000000000..16e6d3cb00 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/offset-access-value-assignment-nullsafe.php @@ -0,0 +1,19 @@ += 8.0 +declare(strict_types = 1); + +namespace OffsetAccessValueAssignmentNullsafe; + +class Bar +{ + public int $val; +} + +function doFoo(?Bar $bar) +{ + /** @var \ArrayAccess $array */ + $array = [ + 'a' => 123, + ]; + + $array['a'] = $bar?->val; +} diff --git a/tests/PHPStan/Rules/Arrays/data/offset-access-value-assignment.php b/tests/PHPStan/Rules/Arrays/data/offset-access-value-assignment.php index 9ce7827d02..2e4f00d7e3 100644 --- a/tests/PHPStan/Rules/Arrays/data/offset-access-value-assignment.php +++ b/tests/PHPStan/Rules/Arrays/data/offset-access-value-assignment.php @@ -44,3 +44,18 @@ public function doLorem(string $str): void } } + +class AppendToArrayAccess +{ + /** @var \ArrayAccess */ + private $collection1; + + /** @var \ArrayAccess&\Countable */ + private $collection2; + + public function foo(): void + { + $this->collection1[] = 1; + $this->collection2[] = 2; + } +} diff --git a/tests/PHPStan/Rules/Arrays/data/unpack-iterable-nullsafe.php b/tests/PHPStan/Rules/Arrays/data/unpack-iterable-nullsafe.php new file mode 100644 index 0000000000..c4b49f2b75 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/unpack-iterable-nullsafe.php @@ -0,0 +1,21 @@ += 8.0 + +namespace UnpackIterableNullsafe; + +class Bar +{ + /** @var int[] */ + public array $array; +} + +class Foo +{ + + public function doFoo(?Bar $bar) + { + $foo = [ + ...$bar?->array, + ]; + } + +} diff --git a/tests/PHPStan/Rules/Cast/EchoRuleTest.php b/tests/PHPStan/Rules/Cast/EchoRuleTest.php index 81621e9f19..0938e0f633 100644 --- a/tests/PHPStan/Rules/Cast/EchoRuleTest.php +++ b/tests/PHPStan/Rules/Cast/EchoRuleTest.php @@ -5,9 +5,10 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class EchoRuleTest extends RuleTestCase { @@ -15,7 +16,7 @@ class EchoRuleTest extends RuleTestCase protected function getRule(): Rule { return new EchoRule( - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false) + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false), ); } @@ -23,7 +24,7 @@ public function testEchoRule(): void { $this->analyse([__DIR__ . '/data/echo.php'], [ [ - 'Parameter #1 (array()) of echo cannot be converted to string.', + 'Parameter #1 (array{}) of echo cannot be converted to string.', 7, ], [ @@ -31,7 +32,7 @@ public function testEchoRule(): void 9, ], [ - 'Parameter #1 (array()) of echo cannot be converted to string.', + 'Parameter #1 (array{}) of echo cannot be converted to string.', 11, ], [ @@ -43,10 +44,24 @@ public function testEchoRule(): void 13, ], [ - 'Parameter #1 (\'string\'|array(\'string\')) of echo cannot be converted to string.', + 'Parameter #1 (\'string\'|array{\'string\'}) of echo cannot be converted to string.', 17, ], ]); } + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/echo-nullsafe.php'], [ + [ + 'Parameter #1 (array|null) of echo cannot be converted to string.', + 15, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Cast/InvalidCastRuleTest.php b/tests/PHPStan/Rules/Cast/InvalidCastRuleTest.php index e0a0fd1e57..67bb979abb 100644 --- a/tests/PHPStan/Rules/Cast/InvalidCastRuleTest.php +++ b/tests/PHPStan/Rules/Cast/InvalidCastRuleTest.php @@ -2,15 +2,18 @@ namespace PHPStan\Rules\Cast; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class InvalidCastRuleTest extends \PHPStan\Testing\RuleTestCase +class InvalidCastRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { $broker = $this->createReflectionProvider(); return new InvalidCastRule($broker, new RuleLevelHelper($broker, true, false, true, false)); @@ -23,14 +26,6 @@ public function testRule(): void 'Cannot cast stdClass to string.', 7, ], - [ - 'Cannot cast array() to int.', - 16, - ], - [ - 'Cannot cast \'blabla\' to int.', - 21, - ], [ 'Cannot cast stdClass to int.', 23, @@ -50,4 +45,23 @@ public function testRule(): void ]); } + public function testBug5162(): void + { + $this->analyse([__DIR__ . '/data/bug-5162.php'], []); + } + + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/invalid-cast-nullsafe.php'], [ + [ + 'Cannot cast stdClass|null to string.', + 13, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php b/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php index 0429ec48ee..3080401757 100644 --- a/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php +++ b/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php @@ -2,19 +2,23 @@ namespace PHPStan\Rules\Cast; +use PhpParser\PrettyPrinter\Standard; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class InvalidPartOfEncapsedStringRuleTest extends \PHPStan\Testing\RuleTestCase +class InvalidPartOfEncapsedStringRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new InvalidPartOfEncapsedStringRule( - new \PhpParser\PrettyPrinter\Standard(), - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false) + new Standard(), + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false), ); } @@ -28,4 +32,18 @@ public function testRule(): void ]); } + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/invalid-encapsed-part-nullsafe.php'], [ + [ + 'Part $bar?->obj (stdClass|null) of encapsed string cannot be cast to string.', + 11, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Cast/PrintRuleTest.php b/tests/PHPStan/Rules/Cast/PrintRuleTest.php index fa12ad4424..1311c3e8ab 100644 --- a/tests/PHPStan/Rules/Cast/PrintRuleTest.php +++ b/tests/PHPStan/Rules/Cast/PrintRuleTest.php @@ -5,9 +5,10 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class PrintRuleTest extends RuleTestCase { @@ -15,7 +16,7 @@ class PrintRuleTest extends RuleTestCase protected function getRule(): Rule { return new PrintRule( - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false) + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false), ); } @@ -23,7 +24,7 @@ public function testPrintRule(): void { $this->analyse([__DIR__ . '/data/print.php'], [ [ - 'Parameter array() of print cannot be converted to string.', + 'Parameter array{} of print cannot be converted to string.', 5, ], [ @@ -35,7 +36,7 @@ public function testPrintRule(): void 9, ], [ - 'Parameter array() of print cannot be converted to string.', + 'Parameter array{} of print cannot be converted to string.', 13, ], [ @@ -47,10 +48,24 @@ public function testPrintRule(): void 17, ], [ - 'Parameter \'string\'|array(\'string\') of print cannot be converted to string.', + 'Parameter \'string\'|array{\'string\'} of print cannot be converted to string.', 21, ], ]); } + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/print-nullsafe.php'], [ + [ + 'Parameter array|null of print cannot be converted to string.', + 15, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Cast/UnsetCastRuleTest.php b/tests/PHPStan/Rules/Cast/UnsetCastRuleTest.php index d9000d4378..923423b1e5 100644 --- a/tests/PHPStan/Rules/Cast/UnsetCastRuleTest.php +++ b/tests/PHPStan/Rules/Cast/UnsetCastRuleTest.php @@ -12,8 +12,7 @@ class UnsetCastRuleTest extends RuleTestCase { - /** @var int */ - private $phpVersion; + private int $phpVersion; protected function getRule(): Rule { @@ -41,7 +40,6 @@ public function dataRule(): array /** * @dataProvider dataRule - * @param int $phpVersion * @param mixed[] $errors */ public function testRule(int $phpVersion, array $errors): void diff --git a/tests/PHPStan/Rules/Cast/data/bug-5162.php b/tests/PHPStan/Rules/Cast/data/bug-5162.php new file mode 100644 index 0000000000..1b38c65ce9 --- /dev/null +++ b/tests/PHPStan/Rules/Cast/data/bug-5162.php @@ -0,0 +1,58 @@ +> + */ + function Get() + { + switch ( rand(1,3) ) + { + case 1: + return ['a' => 'val']; + case 2: + return ['a' => '1']; + case 3: + return ['a' => []]; + } + return []; + } + + public function doFoo(): void + { + // This variant works + $result1 = $this->Get(); + if ( ! array_key_exists('a', $result1)) + { + exit(1); + } + if ( ! is_numeric( $result1['a'] ) ) + { + exit(1); + } + $val = (float) $result1['a']; + } + + public function doBar(): void + { + // This variant doesn't work .. but is logically identical + $result2 = $this->Get(); + if ( array_key_exists('a',$result2) && ! is_numeric( $result2['a'] ) ) + { + exit(1); + } + if ( ! array_key_exists('a', $result2) ) + { + exit(1); + } + $val = (float) $result2['a']; + } + +} diff --git a/tests/PHPStan/Rules/Cast/data/echo-nullsafe.php b/tests/PHPStan/Rules/Cast/data/echo-nullsafe.php new file mode 100644 index 0000000000..05df4374c0 --- /dev/null +++ b/tests/PHPStan/Rules/Cast/data/echo-nullsafe.php @@ -0,0 +1,16 @@ += 8.0 + +declare(strict_types = 1); + +namespace EchoNullsafe; + +class Bar +{ + /** @var int[] */ + public array $array; +} + +function def(?Bar $bar) +{ + echo $bar?->array; +} diff --git a/tests/PHPStan/Rules/Cast/data/invalid-cast-nullsafe.php b/tests/PHPStan/Rules/Cast/data/invalid-cast-nullsafe.php new file mode 100644 index 0000000000..ccdd3b4a61 --- /dev/null +++ b/tests/PHPStan/Rules/Cast/data/invalid-cast-nullsafe.php @@ -0,0 +1,14 @@ += 8.0 + +namespace InvalidCastNullsafe; + +class Bar +{ + public \stdClass $obj; +} + +function doFoo( + ?Bar $bar +) { + (string) $bar?->obj; +}; diff --git a/tests/PHPStan/Rules/Cast/data/invalid-encapsed-part-nullsafe.php b/tests/PHPStan/Rules/Cast/data/invalid-encapsed-part-nullsafe.php new file mode 100644 index 0000000000..25f93d4fd1 --- /dev/null +++ b/tests/PHPStan/Rules/Cast/data/invalid-encapsed-part-nullsafe.php @@ -0,0 +1,12 @@ += 8.0 + +namespace InvalidEncapsedPartNullsafe; + +class Bar +{ + public \stdClass $obj; +} + +function doFoo(?Bar $bar) { + "{$bar?->obj} bar"; +} diff --git a/tests/PHPStan/Rules/Cast/data/print-nullsafe.php b/tests/PHPStan/Rules/Cast/data/print-nullsafe.php new file mode 100644 index 0000000000..444692e425 --- /dev/null +++ b/tests/PHPStan/Rules/Cast/data/print-nullsafe.php @@ -0,0 +1,16 @@ += 8.0 + +declare(strict_types = 1); + +namespace PrintNullsafe; + +class Bar +{ + /** @var int[] */ + public array $array; +} + +function def(?Bar $bar) +{ + print $bar?->array; +} diff --git a/tests/PHPStan/Rules/Classes/AccessPrivateConstantThroughStaticRuleTest.php b/tests/PHPStan/Rules/Classes/AccessPrivateConstantThroughStaticRuleTest.php new file mode 100644 index 0000000000..8c6ff39361 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/AccessPrivateConstantThroughStaticRuleTest.php @@ -0,0 +1,29 @@ + + */ +class AccessPrivateConstantThroughStaticRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new AccessPrivateConstantThroughStaticRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/access-private-constant-static.php'], [ + [ + 'Unsafe access to private constant AccessPrivateConstantThroughStatic\Foo::FOO through static::.', + 12, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php index 8687ce1aa1..755cdeed72 100644 --- a/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassAttributesRuleTest.php @@ -8,9 +8,11 @@ use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -25,18 +27,18 @@ protected function getRule(): Rule new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true), + new RuleLevelHelper($reflectionProvider, true, false, true, false), new NullsafeCheck(), new PhpVersion(80000), - new UnresolvableTypeHelper(true), + new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), true, true, true, true, - true ), - new ClassCaseSensitivityCheck($reflectionProvider, false) - ) + new ClassCaseSensitivityCheck($reflectionProvider, false), + ), ); } @@ -95,6 +97,32 @@ public function testRule(): void 'Unknown parameter $r in call to ClassAttributes\AttributeWithConstructor constructor.', 120, ], + [ + 'Interface ClassAttributes\InterfaceAsAttribute is not an Attribute class.', + 132, + ], + [ + 'Trait ClassAttributes\TraitAsAttribute is not an Attribute class.', + 142, + ], + ]); + } + + public function testRuleForEnums(): void + { + if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/enum-attributes.php'], [ + [ + 'Attribute class EnumAttributes\AttributeWithPropertyTarget does not have the class target.', + 23, + ], + [ + 'Enum EnumAttributes\EnumAsAttribute is not an Attribute class.', + 35, + ], ]); } diff --git a/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php index 84e7e2bede..386d9453ab 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantAttributesRuleTest.php @@ -8,9 +8,11 @@ use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -25,18 +27,18 @@ protected function getRule(): Rule new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true), + new RuleLevelHelper($reflectionProvider, true, false, true, false), new NullsafeCheck(), new PhpVersion(80000), - new UnresolvableTypeHelper(true), + new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), true, true, true, true, - true ), - new ClassCaseSensitivityCheck($reflectionProvider, false) - ) + new ClassCaseSensitivityCheck($reflectionProvider, false), + ), ); } diff --git a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php index 838397f882..d116959773 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php @@ -6,20 +6,21 @@ use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ClassConstantRuleTest extends \PHPStan\Testing\RuleTestCase +class ClassConstantRuleTest extends RuleTestCase { - /** @var int */ - private $phpVersion; + private int $phpVersion; protected function getRule(): Rule { $broker = $this->createReflectionProvider(); - return new ClassConstantRule($broker, new RuleLevelHelper($broker, true, false, true, false), new ClassCaseSensitivityCheck($broker), new PhpVersion($this->phpVersion)); + return new ClassConstantRule($broker, new RuleLevelHelper($broker, true, false, true, false), new ClassCaseSensitivityCheck($broker, true), new PhpVersion($this->phpVersion)); } public function testClassConstant(): void @@ -81,7 +82,7 @@ public function testClassConstant(): void 'Access to undefined constant ClassConstantNamespace\Foo|string::DOLOR.', 33, ], - ] + ], ); } @@ -143,13 +144,17 @@ public function testClassConstantVisibility(): void 'Cannot access constant FOO on int|string.', 116, ], + [ + 'Access to undefined constant static(ClassConstantVisibility\AccessWithStatic)::BAR.', + 129, + ], [ 'Class ClassConstantVisibility\Foo referenced with incorrect case: ClassConstantVisibility\FOO.', - 122, + 135, ], [ 'Access to private constant PRIVATE_FOO of class ClassConstantVisibility\Foo.', - 122, + 135, ], ]); } @@ -222,7 +227,6 @@ public function dataClassConstantOnExpression(): array /** * @dataProvider dataClassConstantOnExpression - * @param int $phpVersion * @param mixed[] $errors */ public function testClassConstantOnExpression(int $phpVersion, array $errors): void @@ -273,4 +277,14 @@ public function testAttributes(): void ]); } + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->phpVersion = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/class-constant-nullsafe.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Classes/DuplicateDeclarationRuleTest.php b/tests/PHPStan/Rules/Classes/DuplicateDeclarationRuleTest.php index 5ea6be4bd8..32a29444be 100644 --- a/tests/PHPStan/Rules/Classes/DuplicateDeclarationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/DuplicateDeclarationRuleTest.php @@ -6,7 +6,7 @@ use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase<\PHPStan\Rules\Classes\DuplicateDeclarationRule> + * @extends RuleTestCase */ class DuplicateDeclarationRuleTest extends RuleTestCase { @@ -51,7 +51,7 @@ public function testDuplicateDeclarations(): void 'Cannot redeclare method DuplicateDeclarations\Foo::Func1().', 35, ], - ] + ], ); } @@ -73,4 +73,26 @@ public function testDuplicatePromotedProperty(): void ]); } + public function testDuplicateEnumCase(): void + { + if (!self::$useStaticReflectionProvider) { + $this->markTestSkipped('This test needs static reflection'); + } + + $this->analyse([__DIR__ . '/data/duplicate-enum-cases.php'], [ + [ + 'Cannot redeclare enum case DuplicatedEnumCase\Foo::BAR.', + 10, + ], + [ + 'Cannot redeclare enum case DuplicatedEnumCase\Boo::BAR.', + 17, + ], + [ + 'Cannot redeclare constant DuplicatedEnumCase\Hoo::BAR.', + 23, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php b/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php new file mode 100644 index 0000000000..0e2a473353 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php @@ -0,0 +1,78 @@ + + */ +class EnumSanityRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new EnumSanityRule(); + } + + public function testRule(): void + { + if (!self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires static reflection'); + } + + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/enum-sanity.php'], [ + [ + 'Enum EnumSanity\EnumWithAbstractMethod contains abstract method foo().', + 7, + ], + [ + 'Enum EnumSanity\EnumWithConstructorAndDestructor contains constructor.', + 12, + ], + [ + 'Enum EnumSanity\EnumWithConstructorAndDestructor contains destructor.', + 15, + ], + [ + 'Enum EnumSanity\EnumWithMagicMethods contains magic method __get().', + 21, + ], + [ + 'Enum EnumSanity\EnumWithMagicMethods contains magic method __set().', + 30, + ], + [ + 'Enum EnumSanity\PureEnumCannotRedeclareMethods cannot redeclare native method cases().', + 39, + ], + [ + 'Enum EnumSanity\BackedEnumCannotRedeclareMethods cannot redeclare native method cases().', + 54, + ], + [ + 'Enum EnumSanity\BackedEnumCannotRedeclareMethods cannot redeclare native method tryFrom().', + 58, + ], + [ + 'Enum EnumSanity\BackedEnumCannotRedeclareMethods cannot redeclare native method from().', + 62, + ], + [ + 'Backed enum EnumSanity\BackedEnumWithFloatType can have only "int" or "string" type.', + 67, + ], + [ + 'Backed enum EnumSanity\BackedEnumWithBoolType can have only "int" or "string" type.', + 71, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php index 836894513f..f430161628 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassInClassExtendsRuleTest.php @@ -4,19 +4,21 @@ use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassInClassExtendsRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingClassInClassExtendsRuleTest extends RuleTestCase { protected function getRule(): Rule { $broker = $this->createReflectionProvider(); return new ExistingClassInClassExtendsRule( - new ClassCaseSensitivityCheck($broker), - $broker + new ClassCaseSensitivityCheck($broker, true), + $broker, ); } @@ -27,6 +29,10 @@ public function testRule(): void 'Class ExtendsImplements\Foo referenced with incorrect case: ExtendsImplements\FOO.', 15, ], + [ + 'Class ExtendsImplements\ExtendsFinalWithAnnotation extends @final class ExtendsImplements\FinalWithAnnotation.', + 43, + ], ]); } @@ -61,4 +67,32 @@ public function testRuleExtendsError(): void ]); } + public function testFinalByTag(): void + { + $this->analyse([__DIR__ . '/data/extends-final-by-tag.php'], [ + [ + 'Class ExtendsFinalByTag\Bar2 extends @final class ExtendsFinalByTag\Bar.', + 21, + ], + ]); + } + + public function testEnums(): void + { + if (!self::$useStaticReflectionProvider || PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs static reflection and PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/class-extends-enum.php'], [ + [ + 'Class ClassExtendsEnum\Foo extends enum ClassExtendsEnum\FooEnum.', + 10, + ], + [ + 'Anonymous class extends enum ClassExtendsEnum\FooEnum.', + 16, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/ExistingClassInInstanceOfRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassInInstanceOfRuleTest.php index 18d571f3e8..e346e8cb1a 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassInInstanceOfRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassInInstanceOfRuleTest.php @@ -4,11 +4,12 @@ use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassInInstanceOfRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingClassInInstanceOfRuleTest extends RuleTestCase { protected function getRule(): Rule @@ -16,8 +17,8 @@ protected function getRule(): Rule $broker = $this->createReflectionProvider(); return new ExistingClassInInstanceOfRule( $broker, - new ClassCaseSensitivityCheck($broker), - true + new ClassCaseSensitivityCheck($broker, true), + true, ); } @@ -50,7 +51,7 @@ public function testClassDoesNotExist(): void 'Using self outside of class scope.', 17, ], - ] + ], ); } diff --git a/tests/PHPStan/Rules/Classes/ExistingClassInTraitUseRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassInTraitUseRuleTest.php index 826094c52a..616fbce43c 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassInTraitUseRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassInTraitUseRuleTest.php @@ -4,19 +4,21 @@ use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassInTraitUseRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingClassInTraitUseRuleTest extends RuleTestCase { protected function getRule(): Rule { $broker = $this->createReflectionProvider(); return new ExistingClassInTraitUseRule( - new ClassCaseSensitivityCheck($broker), - $broker + new ClassCaseSensitivityCheck($broker, true), + $broker, ); } @@ -66,4 +68,22 @@ public function testTraitUseError(): void ]); } + public function testEnums(): void + { + if (!self::$useStaticReflectionProvider || PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs static reflection and PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/trait-use-enum.php'], [ + [ + 'Class TraitUseEnum\Foo uses enum TraitUseEnum\FooEnum.', + 13, + ], + [ + 'Anonymous class uses enum TraitUseEnum\FooEnum.', + 20, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/ExistingClassesInClassImplementsRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassesInClassImplementsRuleTest.php index 72f58b2369..2e8a77f71a 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassesInClassImplementsRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassesInClassImplementsRuleTest.php @@ -4,19 +4,21 @@ use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassesInClassImplementsRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingClassesInClassImplementsRuleTest extends RuleTestCase { protected function getRule(): Rule { $broker = $this->createReflectionProvider(); return new ExistingClassesInClassImplementsRule( - new ClassCaseSensitivityCheck($broker), - $broker + new ClassCaseSensitivityCheck($broker, true), + $broker, ); } @@ -57,4 +59,22 @@ public function testRuleImplementsError(): void ]); } + public function testEnums(): void + { + if (!self::$useStaticReflectionProvider || PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs static reflection and PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/class-implements-enum.php'], [ + [ + 'Class ClassImplementsEnum\Foo implements enum ClassImplementsEnum\FooEnum.', + 10, + ], + [ + 'Anonymous class implements enum ClassImplementsEnum\FooEnum.', + 16, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/ExistingClassesInEnumImplementsRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassesInEnumImplementsRuleTest.php new file mode 100644 index 0000000000..f2336b860c --- /dev/null +++ b/tests/PHPStan/Rules/Classes/ExistingClassesInEnumImplementsRuleTest.php @@ -0,0 +1,65 @@ + + */ +class ExistingClassesInEnumImplementsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + + return new ExistingClassesInEnumImplementsRule( + new ClassCaseSensitivityCheck($reflectionProvider, true), + $reflectionProvider, + ); + } + + public function testRule(): void + { + if (!self::$useStaticReflectionProvider || PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1 and static reflection.'); + } + + $this->analyse([__DIR__ . '/data/enum-implements.php'], [ + [ + 'Interface EnumImplements\FooInterface referenced with incorrect case: EnumImplements\FOOInterface.', + 30, + ], + [ + 'Enum EnumImplements\Foo3 implements class EnumImplements\FooClass.', + 35, + ], + [ + 'Enum EnumImplements\Foo4 implements trait EnumImplements\FooTrait.', + 40, + ], + [ + 'Enum EnumImplements\Foo5 implements enum EnumImplements\FooEnum.', + 45, + ], + [ + 'Enum EnumImplements\Foo6 implements unknown interface EnumImplements\NonexistentInterface.', + 50, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Enum EnumImplements\FooEnum referenced with incorrect case: EnumImplements\FOOEnum.', + 55, + ], + [ + 'Enum EnumImplements\Foo7 implements enum EnumImplements\FooEnum.', + 55, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/ExistingClassesInInterfaceExtendsRuleTest.php b/tests/PHPStan/Rules/Classes/ExistingClassesInInterfaceExtendsRuleTest.php index 196edc0421..d1c02403ee 100644 --- a/tests/PHPStan/Rules/Classes/ExistingClassesInInterfaceExtendsRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ExistingClassesInInterfaceExtendsRuleTest.php @@ -4,19 +4,21 @@ use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassesInInterfaceExtendsRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingClassesInInterfaceExtendsRuleTest extends RuleTestCase { protected function getRule(): Rule { $broker = $this->createReflectionProvider(); return new ExistingClassesInInterfaceExtendsRule( - new ClassCaseSensitivityCheck($broker), - $broker + new ClassCaseSensitivityCheck($broker, true), + $broker, ); } @@ -53,4 +55,18 @@ public function testRuleExtendsError(): void ]); } + public function testEnums(): void + { + if (!self::$useStaticReflectionProvider || PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs static reflection and PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/interface-extends-enum.php'], [ + [ + 'Interface InterfaceExtendsEnum\Foo extends enum InterfaceExtendsEnum\FooEnum.', + 10, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php index 8f6c11818f..dde8dcb7b6 100644 --- a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php @@ -2,19 +2,20 @@ namespace PHPStan\Rules\Classes; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ImpossibleInstanceOfRuleTest extends \PHPStan\Testing\RuleTestCase +class ImpossibleInstanceOfRuleTest extends RuleTestCase { - /** @var bool */ - private $checkAlwaysTrueInstanceOf; + private bool $checkAlwaysTrueInstanceOf; - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $treatPhpDocTypesAsCertain; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new ImpossibleInstanceOfRule($this->checkAlwaysTrueInstanceOf, $this->treatPhpDocTypesAsCertain); } @@ -118,7 +119,6 @@ public function testInstanceof(): void [ 'Instanceof between *NEVER* and ImpossibleInstanceOf\Foo will always evaluate to false.', 234, - $tipText, ], [ 'Instanceof between ImpossibleInstanceOf\Bar&ImpossibleInstanceOf\Foo and ImpossibleInstanceOf\Foo will always evaluate to true.', @@ -178,7 +178,7 @@ public function testInstanceof(): void 433, $tipText, ], - ] + ], ); } @@ -226,7 +226,6 @@ public function testInstanceofWithoutAlwaysTrue(): void [ 'Instanceof between *NEVER* and ImpossibleInstanceOf\Foo will always evaluate to false.', 234, - $tipText, ], [ 'Instanceof between *NEVER* and ImpossibleInstanceOf\Bar will always evaluate to false.', @@ -271,7 +270,7 @@ public function testInstanceofWithoutAlwaysTrue(): void 432, $tipText, ], - ] + ], ); } diff --git a/tests/PHPStan/Rules/Classes/InstantiationCallableRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationCallableRuleTest.php new file mode 100644 index 0000000000..427c0b3a91 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/InstantiationCallableRuleTest.php @@ -0,0 +1,32 @@ + + */ +class InstantiationCallableRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new InstantiationCallableRule(); + } + + public function testRule(): void + { + if (!self::$useStaticReflectionProvider) { + self::markTestSkipped('Test requires static reflection.'); + } + $this->analyse([__DIR__ . '/data/instantiation-callable.php'], [ + [ + 'Cannot create callable from the new operator.', + 11, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index da6f13eb7d..fc909c4369 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -7,21 +7,25 @@ use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class InstantiationRuleTest extends \PHPStan\Testing\RuleTestCase +class InstantiationRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { $broker = $this->createReflectionProvider(); return new InstantiationRule( $broker, - new FunctionCallParametersCheck(new RuleLevelHelper($broker, true, false, true, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(true), true, true, true, true, true), - new ClassCaseSensitivityCheck($broker) + new FunctionCallParametersCheck(new RuleLevelHelper($broker, true, false, true, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), + new ClassCaseSensitivityCheck($broker, true), ); } @@ -190,7 +194,7 @@ public function testInstantiation(): void 'Class TestInstantiation\ClassExtendingAbstractConstructor constructor invoked with 0 parameters, 1 required.', 273, ], - ] + ], ); } @@ -203,7 +207,7 @@ public function testSoap(): void 'Parameter #2 $string of class SoapFault constructor expects string, int given.', 6, ], - ] + ], ); } @@ -246,21 +250,12 @@ public function testOldStyleConstructorOnPhp7(): void $this->markTestSkipped('Test requires PHP 7.x'); } - $errors = [ + $this->analyse([__DIR__ . '/data/php80-constructor.php'], [ [ 'Class OldStyleConstructorOnPhp8 constructor invoked with 0 parameters, 1 required.', 19, ], - ]; - - if (!self::$useStaticReflectionProvider) { - $errors[] = [ - 'Methods with the same name as their class will not be constructors in a future version of PHP; OldStyleConstructorOnPhp8 has a deprecated constructor', - 3, - ]; - } - - $this->analyse([__DIR__ . '/data/php80-constructor.php'], $errors); + ]); } public function testBug4030(): void @@ -357,4 +352,46 @@ public function testBug4681(): void $this->analyse([__DIR__ . '/data/bug-4681.php'], []); } + public function testFirstClassCallable(): void + { + if (PHP_VERSION_ID < 80100 || !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 8.1 and static reflection.'); + } + + // handled by a different rule + $this->analyse([__DIR__ . '/data/first-class-instantiation-callable.php'], []); + } + + public function testEnumInstantiation(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/enum-instantiation.php'], [ + [ + 'Cannot instantiate enum EnumInstantiation\Foo.', + 9, + ], + [ + 'Cannot instantiate enum EnumInstantiation\Foo.', + 14, + ], + [ + 'Cannot instantiate enum EnumInstantiation\Foo.', + 21, + ], + ]); + } + + public function testBug6370(): void + { + $this->analyse([__DIR__ . '/data/bug-6370.php'], [ + [ + 'Parameter #1 $something of class Bug6370\A constructor expects string, int given.', + 45, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php b/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php index 9023c74faa..37946a3fd6 100644 --- a/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InvalidPromotedPropertiesRuleTest.php @@ -12,8 +12,7 @@ class InvalidPromotedPropertiesRuleTest extends RuleTestCase { - /** @var int */ - private $phpVersion; + private int $phpVersion; protected function getRule(): Rule { diff --git a/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php b/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php index 11262e5187..281e125507 100644 --- a/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php +++ b/tests/PHPStan/Rules/Classes/LocalTypeAliasesRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -17,7 +18,7 @@ protected function getRule(): Rule return new LocalTypeAliasesRule( ['GlobalTypeAlias' => 'int|string'], $this->createReflectionProvider(), - self::getContainer()->getByType(TypeNodeResolver::class) + self::getContainer()->getByType(TypeNodeResolver::class), ); } @@ -84,6 +85,24 @@ public function testRule(): void 'Circular definition detected in type alias CircularTypeAliasImport1.', 47, ], + [ + 'Invalid type definition detected in type alias InvalidTypeAlias.', + 62, + ], + ]); + } + + public function testEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/local-type-aliases-enums.php'], [ + [ + 'Cannot import type alias Test: class LocalTypeAliasesEnums\NonexistentClass does not exist.', + 8, + ], ]); } diff --git a/tests/PHPStan/Rules/Classes/MixinRuleTest.php b/tests/PHPStan/Rules/Classes/MixinRuleTest.php index 2c214fa617..f765e94f43 100644 --- a/tests/PHPStan/Rules/Classes/MixinRuleTest.php +++ b/tests/PHPStan/Rules/Classes/MixinRuleTest.php @@ -8,7 +8,7 @@ use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use PHPStan\Type\FileTypeMapper; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -21,13 +21,12 @@ protected function getRule(): Rule $reflectionProvider = $this->createReflectionProvider(); return new MixinRule( - self::getContainer()->getByType(FileTypeMapper::class), $reflectionProvider, - new ClassCaseSensitivityCheck($reflectionProvider), + new ClassCaseSensitivityCheck($reflectionProvider, true), new GenericObjectTypeCheck(), - new MissingTypehintCheck($reflectionProvider, true, true, true), - new UnresolvableTypeHelper(true), - true + new MissingTypehintCheck($reflectionProvider, true, true, true, []), + new UnresolvableTypeHelper(), + true, ); } @@ -47,7 +46,7 @@ public function testRule(): void 34, ], [ - 'Generic type Traversable in PHPDoc tag @mixin specifies 3 template types, but class Traversable supports only 2: TKey, TValue', + 'Generic type Traversable in PHPDoc tag @mixin specifies 3 template types, but interface Traversable supports only 2: TKey, TValue', 34, ], [ @@ -81,6 +80,24 @@ public function testRule(): void 'Class MixinRule\Foo referenced with incorrect case: MixinRule\foo.', 84, ], + [ + 'PHPDoc tag @mixin contains non-object type int.', + 92, + ], + ]); + } + + public function testEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/mixin-enums.php'], [ + [ + 'PHPDoc tag @mixin contains non-object type int.', + 16, + ], ]); } diff --git a/tests/PHPStan/Rules/Classes/NewStaticRuleTest.php b/tests/PHPStan/Rules/Classes/NewStaticRuleTest.php index 785bdfcfe7..5ba399a0f0 100644 --- a/tests/PHPStan/Rules/Classes/NewStaticRuleTest.php +++ b/tests/PHPStan/Rules/Classes/NewStaticRuleTest.php @@ -3,11 +3,12 @@ namespace PHPStan\Rules\Classes; use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class NewStaticRuleTest extends \PHPStan\Testing\RuleTestCase +class NewStaticRuleTest extends RuleTestCase { protected function getRule(): Rule diff --git a/tests/PHPStan/Rules/Classes/NonClassAttributeClassRuleTest.php b/tests/PHPStan/Rules/Classes/NonClassAttributeClassRuleTest.php index b882278baa..674ed16200 100644 --- a/tests/PHPStan/Rules/Classes/NonClassAttributeClassRuleTest.php +++ b/tests/PHPStan/Rules/Classes/NonClassAttributeClassRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -41,4 +42,18 @@ public function testRule(): void ]); } + public function testEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/enum-cannot-be-attribute.php'], [ + [ + 'Enum cannot be an Attribute class.', + 5, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/TraitAttributeClassRuleTest.php b/tests/PHPStan/Rules/Classes/TraitAttributeClassRuleTest.php index fc08dcfe74..68795831ae 100644 --- a/tests/PHPStan/Rules/Classes/TraitAttributeClassRuleTest.php +++ b/tests/PHPStan/Rules/Classes/TraitAttributeClassRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase diff --git a/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php b/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php index 233a39d859..de702b005d 100644 --- a/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php +++ b/tests/PHPStan/Rules/Classes/UnusedConstructorParametersRuleTest.php @@ -2,18 +2,21 @@ namespace PHPStan\Rules\Classes; +use PHPStan\Rules\Rule; use PHPStan\Rules\UnusedFunctionParametersCheck; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class UnusedConstructorParametersRuleTest extends \PHPStan\Testing\RuleTestCase +class UnusedConstructorParametersRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new UnusedConstructorParametersRule(new UnusedFunctionParametersCheck( - $this->createReflectionProvider() + $this->createReflectionProvider(), )); } diff --git a/tests/PHPStan/Rules/Classes/data/access-private-constant-static.php b/tests/PHPStan/Rules/Classes/data/access-private-constant-static.php new file mode 100644 index 0000000000..84fbffba7c --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/access-private-constant-static.php @@ -0,0 +1,29 @@ += 8.0 + +namespace ClassConstantNullsafeNamespace; + +class Foo { + public const LOREM = 'lorem'; + +} +class Bar +{ + public Foo $foo; +} + +function doFoo(?Bar $bar) +{ + $bar?->foo::LOREM; +} diff --git a/tests/PHPStan/Rules/Classes/data/class-constant-visibility.php b/tests/PHPStan/Rules/Classes/data/class-constant-visibility.php index 6bee5dfb20..f4be1b9a9f 100644 --- a/tests/PHPStan/Rules/Classes/data/class-constant-visibility.php +++ b/tests/PHPStan/Rules/Classes/data/class-constant-visibility.php @@ -118,6 +118,19 @@ public function doIpsum(WithFooConstant $foo) } +class AccessWithStatic +{ + + private const FOO = 1; + + public function doFoo() + { + static::FOO; // reported by a different rule + static::BAR; + } + +} + function () { FOO::PRIVATE_FOO; }; diff --git a/tests/PHPStan/Rules/Classes/data/class-extends-enum.php b/tests/PHPStan/Rules/Classes/data/class-extends-enum.php new file mode 100644 index 0000000000..3031b505cd --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/class-extends-enum.php @@ -0,0 +1,19 @@ += 8.1 + +namespace ClassExtendsEnum; + +enum FooEnum +{ + +} + +class Foo extends FooEnum +{ + +} + +function (): void { + new class() extends FooEnum { + + }; +}; diff --git a/tests/PHPStan/Rules/Classes/data/class-implements-enum.php b/tests/PHPStan/Rules/Classes/data/class-implements-enum.php new file mode 100644 index 0000000000..a6bcab0e7c --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/class-implements-enum.php @@ -0,0 +1,19 @@ += 8.1 + +namespace ClassImplementsEnum; + +enum FooEnum +{ + +} + +class Foo implements FooEnum +{ + +} + +function (): void { + new class() implements FooEnum { + + }; +}; diff --git a/tests/PHPStan/Rules/Classes/data/duplicate-enum-cases.php b/tests/PHPStan/Rules/Classes/data/duplicate-enum-cases.php new file mode 100644 index 0000000000..0b637942c2 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/duplicate-enum-cases.php @@ -0,0 +1,24 @@ += 8.1 + +namespace DuplicatedEnumCase; + +enum Foo +{ + case BAR; + case FOO; + case bar; + case BAR; +} + +enum Boo +{ + const BAR = 0; + const bar = 0; + case BAR; +} + +enum Hoo +{ + case BAR; + const BAR = 0; +} diff --git a/tests/PHPStan/Rules/Classes/data/enum-attributes.php b/tests/PHPStan/Rules/Classes/data/enum-attributes.php new file mode 100644 index 0000000000..d6faaf1d12 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/enum-attributes.php @@ -0,0 +1,39 @@ += 8.1 + +namespace EnumAttributes; + +#[\Attribute] +class AttributeWithoutSpecificTarget +{ + +} + +#[\Attribute(\Attribute::TARGET_PROPERTY)] +class AttributeWithPropertyTarget +{ + +} + +#[AttributeWithoutSpecificTarget] +enum EnumWithValidClassAttribute +{ + +} + +#[AttributeWithPropertyTarget] +enum EnumWithInvalidClassAttribute +{ + +} + +#[\Attribute] +enum EnumAsAttribute +{ + +} + +#[EnumAsAttribute] +class ClassWithEnumAttribute +{ + +} diff --git a/tests/PHPStan/Rules/Classes/data/enum-cannot-be-attribute.php b/tests/PHPStan/Rules/Classes/data/enum-cannot-be-attribute.php new file mode 100644 index 0000000000..21a0f7230d --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/enum-cannot-be-attribute.php @@ -0,0 +1,9 @@ += 8.1 + +namespace EnumAsAttribute; + +#[\Attribute] +enum Foo +{ + +} diff --git a/tests/PHPStan/Rules/Classes/data/enum-implements.php b/tests/PHPStan/Rules/Classes/data/enum-implements.php new file mode 100644 index 0000000000..6ce3e6e85d --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/enum-implements.php @@ -0,0 +1,58 @@ += 8.1 + +namespace EnumImplements; + +interface FooInterface +{ + +} + +class FooClass +{ + +} + +trait FooTrait +{ + +} + +enum FooEnum +{ + +} + +enum Foo implements FooInterface +{ + +} + +enum Foo2 implements FOOInterface +{ + +} + +enum Foo3 implements FooClass +{ + +} + +enum Foo4 implements FooTrait +{ + +} + +enum Foo5 implements FooEnum +{ + +} + +enum Foo6 implements NonexistentInterface +{ + +} + +enum Foo7 implements FOOEnum +{ + +} diff --git a/tests/PHPStan/Rules/Classes/data/enum-instantiation.php b/tests/PHPStan/Rules/Classes/data/enum-instantiation.php new file mode 100644 index 0000000000..708c7b2a31 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/enum-instantiation.php @@ -0,0 +1,23 @@ += 8.1 + +namespace EnumInstantiation; + +enum Foo +{ + public function createSelf() + { + return new self(); + } + + public function createStatic() + { + return new static(); + } +} + +class Boo +{ + public static function createFoo() { + return new Foo(); + } +} diff --git a/tests/PHPStan/Rules/Classes/data/enum-sanity.php b/tests/PHPStan/Rules/Classes/data/enum-sanity.php new file mode 100644 index 0000000000..477ea824d5 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/enum-sanity.php @@ -0,0 +1,73 @@ +branch1 instanceof \SimpleXMLElement; + echo $xml->branch2->branch3 instanceof \SimpleXMLElement; + } + +} diff --git a/tests/PHPStan/Rules/Classes/data/instantiation-callable.php b/tests/PHPStan/Rules/Classes/data/instantiation-callable.php new file mode 100644 index 0000000000..0a3a4d540f --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/instantiation-callable.php @@ -0,0 +1,14 @@ += 8.1 + +namespace InterfaceExtendsEnum; + +enum FooEnum +{ + +} + +interface Foo extends FooEnum +{ + +} diff --git a/tests/PHPStan/Rules/Classes/data/local-type-aliases-enums.php b/tests/PHPStan/Rules/Classes/data/local-type-aliases-enums.php new file mode 100644 index 0000000000..f2e0c58915 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/local-type-aliases-enums.php @@ -0,0 +1,11 @@ += 8.1 + +namespace LocalTypeAliasesEnums; + +/** + * @phpstan-import-type Test from NonexistentClass + */ +enum Foo +{ + +} diff --git a/tests/PHPStan/Rules/Classes/data/local-type-aliases.php b/tests/PHPStan/Rules/Classes/data/local-type-aliases.php index 316a52c323..44ed116a70 100644 --- a/tests/PHPStan/Rules/Classes/data/local-type-aliases.php +++ b/tests/PHPStan/Rules/Classes/data/local-type-aliases.php @@ -55,3 +55,10 @@ class Qux class Generic { } + +/** + * @phpstan-type InvalidTypeAlias invalid-type-definition + */ +class Invalid +{ +} diff --git a/tests/PHPStan/Rules/Classes/data/mixin-enums.php b/tests/PHPStan/Rules/Classes/data/mixin-enums.php new file mode 100644 index 0000000000..cc35ec2b48 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/mixin-enums.php @@ -0,0 +1,24 @@ += 8.1 + +namespace MixinEnums; + +/** + * @mixin \Exception + */ +enum Foo +{ + +} + +/** + * @mixin int + */ +enum Bar +{ + +} + +enum Baz +{ + +} diff --git a/tests/PHPStan/Rules/Classes/data/mixin.php b/tests/PHPStan/Rules/Classes/data/mixin.php index 63fa7cb7e4..c770d2f0a1 100644 --- a/tests/PHPStan/Rules/Classes/data/mixin.php +++ b/tests/PHPStan/Rules/Classes/data/mixin.php @@ -85,3 +85,11 @@ class Amet { } + +/** + * @mixin int + */ +interface InterfaceWithMixin +{ + +} diff --git a/tests/PHPStan/Rules/Classes/data/trait-use-enum.php b/tests/PHPStan/Rules/Classes/data/trait-use-enum.php new file mode 100644 index 0000000000..6e741d3909 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/trait-use-enum.php @@ -0,0 +1,23 @@ += 8.1 + +namespace TraitUseEnum; + +enum FooEnum +{ + +} + +class Foo +{ + + use FooEnum; + +} + +function (): void { + new class() { + + use FooEnum; + + }; +}; diff --git a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php index 26c45e4313..263f0db8d4 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php @@ -2,16 +2,18 @@ namespace PHPStan\Rules\Comparison; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class BooleanAndConstantConditionRuleTest extends \PHPStan\Testing\RuleTestCase +class BooleanAndConstantConditionRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $treatPhpDocTypesAsCertain; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new BooleanAndConstantConditionRule( new ConstantConditionRuleHelper( @@ -19,12 +21,11 @@ protected function getRule(): \PHPStan\Rules\Rule $this->createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), $this->treatPhpDocTypesAsCertain, - true ); } @@ -55,7 +56,7 @@ public function testRule(): void 27, ], [ - 'Right side of && is always false.', + 'Result of && is always false.', 30, ], [ @@ -176,7 +177,6 @@ public function dataTreatPhpDocTypesAsCertainRegression(): array /** * @dataProvider dataTreatPhpDocTypesAsCertainRegression - * @param bool $treatPhpDocTypesAsCertain */ public function testTreatPhpDocTypesAsCertainRegression(bool $treatPhpDocTypesAsCertain): void { @@ -201,4 +201,27 @@ public function testBug2231(): void ]); } + public function testBug1746(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-1746.php'], [ + [ + 'Left side of && is always true.', + 20, + ], + ]); + } + + public function testBug4666(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4666.php'], []); + } + + public function testBug2870(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-2870.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php index bfa0081cd6..94c373f7ea 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php @@ -2,16 +2,19 @@ namespace PHPStan\Rules\Comparison; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class BooleanNotConstantConditionRuleTest extends \PHPStan\Testing\RuleTestCase +class BooleanNotConstantConditionRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $treatPhpDocTypesAsCertain; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new BooleanNotConstantConditionRule( new ConstantConditionRuleHelper( @@ -19,11 +22,11 @@ protected function getRule(): \PHPStan\Rules\Rule $this->createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ); } @@ -104,7 +107,6 @@ public function dataTreatPhpDocTypesAsCertainRegression(): array /** * @dataProvider dataTreatPhpDocTypesAsCertainRegression - * @param bool $treatPhpDocTypesAsCertain */ public function testTreatPhpDocTypesAsCertainRegression(bool $treatPhpDocTypesAsCertain): void { @@ -112,4 +114,14 @@ public function testTreatPhpDocTypesAsCertainRegression(bool $treatPhpDocTypesAs $this->analyse([__DIR__ . '/../DeadCode/data/bug-without-issue-1.php'], []); } + public function testBug6473(): void + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6473.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php index f02dc93cbb..c2fffe45d7 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php @@ -2,16 +2,19 @@ namespace PHPStan\Rules\Comparison; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class BooleanOrConstantConditionRuleTest extends \PHPStan\Testing\RuleTestCase +class BooleanOrConstantConditionRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $treatPhpDocTypesAsCertain; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new BooleanOrConstantConditionRule( new ConstantConditionRuleHelper( @@ -19,12 +22,11 @@ protected function getRule(): \PHPStan\Rules\Rule $this->createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), $this->treatPhpDocTypesAsCertain, - true ); } @@ -59,7 +61,7 @@ public function testRule(): void 30, ], [ - 'Right side of || is always false.', + 'Result of || is always true.', 33, ], [ @@ -167,7 +169,6 @@ public function dataTreatPhpDocTypesAsCertainRegression(): array /** * @dataProvider dataTreatPhpDocTypesAsCertainRegression - * @param bool $treatPhpDocTypesAsCertain */ public function testTreatPhpDocTypesAsCertainRegression(bool $treatPhpDocTypesAsCertain): void { @@ -175,4 +176,14 @@ public function testTreatPhpDocTypesAsCertainRegression(bool $treatPhpDocTypesAs $this->analyse([__DIR__ . '/data/boolean-or-treat-phpdoc-types-regression.php'], []); } + public function testBug6258(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6258.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php new file mode 100644 index 0000000000..77ce3dad2c --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php @@ -0,0 +1,67 @@ + + */ +class DoWhileLoopConstantConditionRuleTest extends RuleTestCase +{ + + private bool $treatPhpDocTypesAsCertain = true; + + protected function getRule(): Rule + { + return new DoWhileLoopConstantConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + $this->createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), + $this->treatPhpDocTypesAsCertain, + ), + $this->treatPhpDocTypesAsCertain, + ); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return $this->treatPhpDocTypesAsCertain; + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/do-while-loop.php'], [ + [ + 'Do-while loop condition is always true.', + 12, + ], + [ + 'Do-while loop condition is always false.', + 37, + ], + [ + 'Do-while loop condition is always false.', + 46, + ], + [ + 'Do-while loop condition is always false.', + 55, + ], + [ + 'Do-while loop condition is always true.', + 64, + ], + [ + 'Do-while loop condition is always false.', + 73, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php index 31cb731e65..9683c239a0 100644 --- a/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php @@ -2,16 +2,18 @@ namespace PHPStan\Rules\Comparison; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ElseIfConstantConditionRuleTest extends \PHPStan\Testing\RuleTestCase +class ElseIfConstantConditionRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $treatPhpDocTypesAsCertain; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new ElseIfConstantConditionRule( new ConstantConditionRuleHelper( @@ -19,11 +21,11 @@ protected function getRule(): \PHPStan\Rules\Rule $this->createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ); } diff --git a/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php index 4ad516f3ce..03b7bd365d 100644 --- a/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php @@ -2,23 +2,18 @@ namespace PHPStan\Rules\Comparison; -use PhpParser\Node\Expr\FuncCall; -use PHPStan\Analyser\Scope; -use PHPStan\Reflection\FunctionReflection; -use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\Type; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class IfConstantConditionRuleTest extends \PHPStan\Testing\RuleTestCase +class IfConstantConditionRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $treatPhpDocTypesAsCertain; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new IfConstantConditionRule( new ConstantConditionRuleHelper( @@ -26,11 +21,11 @@ protected function getRule(): \PHPStan\Rules\Rule $this->createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ); } @@ -39,28 +34,6 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool return $this->treatPhpDocTypesAsCertain; } - /** - * @return DynamicFunctionReturnTypeExtension[] - */ - public function getDynamicFunctionReturnTypeExtensions(): array - { - return [ - new class implements DynamicFunctionReturnTypeExtension { - - public function isFunctionSupported(FunctionReflection $functionReflection): bool - { - return $functionReflection->getName() === 'always_true'; - } - - public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type - { - return new ConstantBooleanType(true); - } - - }, - ]; - } - public function testRule(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 8e7633a5df..f057e1aa81 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -2,29 +2,32 @@ namespace PHPStan\Rules\Comparison; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use stdClass; +use const PHP_VERSION_ID; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ImpossibleCheckTypeFunctionCallRuleTest extends \PHPStan\Testing\RuleTestCase +class ImpossibleCheckTypeFunctionCallRuleTest extends RuleTestCase { - /** @var bool */ - private $checkAlwaysTrueCheckTypeFunctionCall; + private bool $checkAlwaysTrueCheckTypeFunctionCall; - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $treatPhpDocTypesAsCertain; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new ImpossibleCheckTypeFunctionCallRule( new ImpossibleCheckTypeHelper( $this->createReflectionProvider(), $this->getTypeSpecifier(), - [\stdClass::class], - $this->treatPhpDocTypesAsCertain + [stdClass::class], + $this->treatPhpDocTypesAsCertain, ), $this->checkAlwaysTrueCheckTypeFunctionCall, - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ); } @@ -102,43 +105,43 @@ public function testImpossibleCheckTypeFunctionCall(): void 212, ], [ - 'Call to function in_array() with arguments int, array(\'foo\', \'bar\') and true will always evaluate to false.', + 'Call to function in_array() with arguments int, array{\'foo\', \'bar\'} and true will always evaluate to false.', 235, ], [ - 'Call to function in_array() with arguments \'bar\'|\'foo\', array(\'baz\', \'lorem\') and true will always evaluate to false.', + 'Call to function in_array() with arguments \'bar\'|\'foo\', array{\'baz\', \'lorem\'} and true will always evaluate to false.', 244, ], [ - 'Call to function in_array() with arguments \'bar\'|\'foo\', array(\'foo\', \'bar\') and true will always evaluate to true.', + 'Call to function in_array() with arguments \'bar\'|\'foo\', array{\'foo\', \'bar\'} and true will always evaluate to true.', 248, ], [ - 'Call to function in_array() with arguments \'foo\', array(\'foo\') and true will always evaluate to true.', + 'Call to function in_array() with arguments \'foo\', array{\'foo\'} and true will always evaluate to true.', 252, ], [ - 'Call to function in_array() with arguments \'foo\', array(\'foo\', \'bar\') and true will always evaluate to true.', + 'Call to function in_array() with arguments \'foo\', array{\'foo\', \'bar\'} and true will always evaluate to true.', 256, ], [ - 'Call to function in_array() with arguments \'bar\', array()|array(\'foo\') and true will always evaluate to false.', + 'Call to function in_array() with arguments \'bar\', array{}|array{\'foo\'} and true will always evaluate to false.', 320, ], [ - 'Call to function in_array() with arguments \'baz\', array(0 => \'bar\', ?1 => \'foo\') and true will always evaluate to false.', + 'Call to function in_array() with arguments \'baz\', array{0: \'bar\', 1?: \'foo\'} and true will always evaluate to false.', 336, ], [ - 'Call to function in_array() with arguments \'foo\', array() and true will always evaluate to false.', + 'Call to function in_array() with arguments \'foo\', array{} and true will always evaluate to false.', 343, ], [ - 'Call to function array_key_exists() with \'a\' and array(\'a\' => 1, ?\'b\' => 2) will always evaluate to true.', + 'Call to function array_key_exists() with \'a\' and array{a: 1, b?: 2} will always evaluate to true.', 360, ], [ - 'Call to function array_key_exists() with \'c\' and array(\'a\' => 1, ?\'b\' => 2) will always evaluate to false.', + 'Call to function array_key_exists() with \'c\' and array{a: 1, b?: 2} will always evaluate to false.', 366, ], [ @@ -238,7 +241,7 @@ public function testImpossibleCheckTypeFunctionCall(): void 'Call to function property_exists() with CheckTypeFunctionCall\Bug2221 and \'foo\' will always evaluate to true.', 786, ], - ] + ], ); } @@ -279,27 +282,27 @@ public function testImpossibleCheckTypeFunctionCallWithoutAlwaysTrue(): void 212, ], [ - 'Call to function in_array() with arguments int, array(\'foo\', \'bar\') and true will always evaluate to false.', + 'Call to function in_array() with arguments int, array{\'foo\', \'bar\'} and true will always evaluate to false.', 235, ], [ - 'Call to function in_array() with arguments \'bar\'|\'foo\', array(\'baz\', \'lorem\') and true will always evaluate to false.', + 'Call to function in_array() with arguments \'bar\'|\'foo\', array{\'baz\', \'lorem\'} and true will always evaluate to false.', 244, ], [ - 'Call to function in_array() with arguments \'bar\', array()|array(\'foo\') and true will always evaluate to false.', + 'Call to function in_array() with arguments \'bar\', array{}|array{\'foo\'} and true will always evaluate to false.', 320, ], [ - 'Call to function in_array() with arguments \'baz\', array(0 => \'bar\', ?1 => \'foo\') and true will always evaluate to false.', + 'Call to function in_array() with arguments \'baz\', array{0: \'bar\', 1?: \'foo\'} and true will always evaluate to false.', 336, ], [ - 'Call to function in_array() with arguments \'foo\', array() and true will always evaluate to false.', + 'Call to function in_array() with arguments \'foo\', array{} and true will always evaluate to false.', 343, ], [ - 'Call to function array_key_exists() with \'c\' and array(\'a\' => 1, ?\'b\' => 2) will always evaluate to false.', + 'Call to function array_key_exists() with \'c\' and array{a: 1, b?: 2} will always evaluate to false.', 366, ], [ @@ -338,7 +341,7 @@ public function testImpossibleCheckTypeFunctionCallWithoutAlwaysTrue(): void 'Call to function is_numeric() with \'blabla\' will always evaluate to false.', 693, ], - ] + ], ); } @@ -413,4 +416,121 @@ public function testBug4999(): void $this->analyse([__DIR__ . '/data/bug-4999.php'], []); } + public function testArrayIsList(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/array-is-list.php'], [ + [ + 'Call to function array_is_list() with array will always evaluate to false.', + 13, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + 'Call to function array_is_list() with array{foo: \'bar\', bar: \'baz\'} will always evaluate to false.', + 40, + ], + [ + 'Call to function array_is_list() with array{0: \'foo\', foo: \'bar\', bar: \'baz\'} will always evaluate to false.', + 44, + ], + ]); + } + + public function testBug3766(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-3766.php'], []); + } + + public function testBug6305(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6305.php'], [ + [ + 'Call to function is_subclass_of() with Bug6305\B and \'Bug6305\\\A\' will always evaluate to true.', + 11, + ], + [ + 'Call to function is_subclass_of() with Bug6305\B and \'Bug6305\\\B\' will always evaluate to false.', + 14, + ], + ]); + } + + public function testBug6698(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6698.php'], []); + } + + public function testBug5369(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-5369.php'], []); + } + + public function testBugInArrayDateFormat(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/in-array-date-format.php'], [ + [ + 'Call to function in_array() with arguments \'a\', non-empty-array and true will always evaluate to true.', + 39, + ], + [ + 'Call to function in_array() with arguments \'b\', non-empty-array and true will always evaluate to false.', + 43, + ], + [ + 'Call to function in_array() with arguments int, array{} and true will always evaluate to false.', + 47, + ], + ]); + } + + public function testBug5496(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-5496.php'], []); + } + + public function testBug3892(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-3892.php'], []); + } + + public function testBug3314(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-3314.php'], []); + } + + public function testBug2870(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-2870.php'], []); + } + + public function testBug5354(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-5354.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php new file mode 100644 index 0000000000..7d12e3f769 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php @@ -0,0 +1,40 @@ + + */ +class ImpossibleCheckTypeGenericOverwriteRuleTest extends RuleTestCase +{ + + public function getRule(): Rule + { + return new ImpossibleCheckTypeMethodCallRule( + new ImpossibleCheckTypeHelper( + $this->createReflectionProvider(), + $this->getTypeSpecifier(), + [], + true, + ), + true, + true, + ); + } + + public function testNoReportedErrorOnOverwrite(): void + { + $this->analyse([__DIR__ . '/data/generic-type-override.php'], []); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/impossible-check-type-generic-overwrite.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php new file mode 100644 index 0000000000..ef32567c9f --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php @@ -0,0 +1,113 @@ + + */ +class ImpossibleCheckTypeMethodCallRuleEqualsTest extends RuleTestCase +{ + + public function getRule(): Rule + { + return new ImpossibleCheckTypeMethodCallRule( + new ImpossibleCheckTypeHelper( + $this->createReflectionProvider(), + $this->getTypeSpecifier(), + [], + true, + ), + true, + true, + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/impossible-method-call.php'], [ + [ + 'Call to method PHPStan\Tests\AssertionClass::assertString() with string will always evaluate to true.', + 14, + ], + [ + 'Call to method PHPStan\Tests\AssertionClass::assertString() with int will always evaluate to false.', + 15, + ], + [ + 'Call to method PHPStan\Tests\AssertionClass::assertNotInt() with int will always evaluate to false.', + 30, + ], + [ + 'Call to method PHPStan\Tests\AssertionClass::assertNotInt() with string will always evaluate to true.', + 36, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with 1 and 1 will always evaluate to true.', + 60, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with 1 and 2 will always evaluate to false.', + 63, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with 1 and 1 will always evaluate to false.', + 66, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with 1 and 2 will always evaluate to true.', + 69, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with stdClass and stdClass will always evaluate to true.', + 78, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with stdClass and stdClass will always evaluate to false.', + 81, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with \'foo\' and \'foo\' will always evaluate to true.', + 101, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with \'foo\' and \'foo\' will always evaluate to false.', + 104, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with array{} and array{} will always evaluate to true.', + 113, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with array{} and array{} will always evaluate to false.', + 116, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with \'\' and \'\' will always evaluate to true.', + 174, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with \'\' and \'\' will always evaluate to false.', + 175, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with 1 and 1 will always evaluate to true.', + 191, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with 2 and 2 will always evaluate to false.', + 194, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/impossible-check-type-method-call-equals.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php index 633a48b728..5db54072a4 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php @@ -2,36 +2,28 @@ namespace PHPStan\Rules\Comparison; -use PhpParser\Node\Expr\MethodCall; -use PHPStan\Analyser\Scope; -use PHPStan\Analyser\SpecifiedTypes; -use PHPStan\Analyser\TypeSpecifier; -use PHPStan\Analyser\TypeSpecifierAwareExtension; -use PHPStan\Analyser\TypeSpecifierContext; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Tests\AssertionClassMethodTypeSpecifyingExtension; -use PHPStan\Type\MethodTypeSpecifyingExtension; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ImpossibleCheckTypeMethodCallRuleTest extends \PHPStan\Testing\RuleTestCase +class ImpossibleCheckTypeMethodCallRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $treatPhpDocTypesAsCertain; - public function getRule(): \PHPStan\Rules\Rule + public function getRule(): Rule { return new ImpossibleCheckTypeMethodCallRule( new ImpossibleCheckTypeHelper( $this->createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), true, - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ); } @@ -40,152 +32,6 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool return $this->treatPhpDocTypesAsCertain; } - /** - * @return MethodTypeSpecifyingExtension[] - */ - protected function getMethodTypeSpecifyingExtensions(): array - { - return [ - new AssertionClassMethodTypeSpecifyingExtension(null), - new class() implements MethodTypeSpecifyingExtension, - TypeSpecifierAwareExtension { - - /** @var TypeSpecifier */ - private $typeSpecifier; - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - - public function getClass(): string - { - return \PHPStan\Tests\AssertionClass::class; - } - - public function isMethodSupported( - MethodReflection $methodReflection, - MethodCall $node, - TypeSpecifierContext $context - ): bool - { - return $methodReflection->getName() === 'assertNotInt' - && count($node->args) > 0; - } - - public function specifyTypes( - MethodReflection $methodReflection, - MethodCall $node, - Scope $scope, - TypeSpecifierContext $context - ): SpecifiedTypes - { - return $this->typeSpecifier->specifyTypesInCondition( - $scope, - new \PhpParser\Node\Expr\BooleanNot( - new \PhpParser\Node\Expr\FuncCall( - new \PhpParser\Node\Name('is_int'), - [ - $node->args[0], - ] - ) - ), - TypeSpecifierContext::createTruthy() - ); - } - - }, - new class() implements MethodTypeSpecifyingExtension, - TypeSpecifierAwareExtension { - - /** @var TypeSpecifier */ - private $typeSpecifier; - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - - public function getClass(): string - { - return \ImpossibleMethodCall\Foo::class; - } - - public function isMethodSupported( - MethodReflection $methodReflection, - MethodCall $node, - TypeSpecifierContext $context - ): bool - { - return $methodReflection->getName() === 'isSame' - && count($node->args) >= 2; - } - - public function specifyTypes( - MethodReflection $methodReflection, - MethodCall $node, - Scope $scope, - TypeSpecifierContext $context - ): SpecifiedTypes - { - return $this->typeSpecifier->specifyTypesInCondition( - $scope, - new \PhpParser\Node\Expr\BinaryOp\Identical( - $node->args[0]->value, - $node->args[1]->value - ), - TypeSpecifierContext::createTruthy() - ); - } - - }, - new class() implements MethodTypeSpecifyingExtension, - TypeSpecifierAwareExtension { - - /** @var TypeSpecifier */ - private $typeSpecifier; - - public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void - { - $this->typeSpecifier = $typeSpecifier; - } - - public function getClass(): string - { - return \ImpossibleMethodCall\Foo::class; - } - - public function isMethodSupported( - MethodReflection $methodReflection, - MethodCall $node, - TypeSpecifierContext $context - ): bool - { - return $methodReflection->getName() === 'isNotSame' - && count($node->args) >= 2; - } - - public function specifyTypes( - MethodReflection $methodReflection, - MethodCall $node, - Scope $scope, - TypeSpecifierContext $context - ): SpecifiedTypes - { - return $this->typeSpecifier->specifyTypesInCondition( - $scope, - new \PhpParser\Node\Expr\BinaryOp\NotIdentical( - $node->args[0]->value, - $node->args[1]->value - ), - TypeSpecifierContext::createTruthy() - ); - } - - }, - ]; - } - public function testRule(): void { $this->treatPhpDocTypesAsCertain = true; @@ -226,6 +72,82 @@ public function testRule(): void 'Call to method ImpossibleMethodCall\Foo::isSame() with stdClass and stdClass will always evaluate to true.', 78, ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with stdClass and stdClass will always evaluate to false.', + 81, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with \'foo\' and \'foo\' will always evaluate to true.', + 101, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with \'foo\' and \'foo\' will always evaluate to false.', + 104, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with array{} and array{} will always evaluate to true.', + 113, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with array{} and array{} will always evaluate to false.', + 116, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with array{1, 3} and array{1, 3} will always evaluate to true.', + 119, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with array{1, 3} and array{1, 3} will always evaluate to false.', + 122, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with 1 and stdClass will always evaluate to false.', + 126, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with 1 and stdClass will always evaluate to true.', + 130, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with \'1\' and stdClass will always evaluate to false.', + 133, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with \'1\' and stdClass will always evaluate to true.', + 136, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with array{\'a\', \'b\'} and array{1, 2} will always evaluate to false.', + 139, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with array{\'a\', \'b\'} and array{1, 2} will always evaluate to true.', + 142, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with stdClass and \'1\' will always evaluate to false.', + 145, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with stdClass and \'1\' will always evaluate to true.', + 148, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with \'\' and \'\' will always evaluate to true.', + 174, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with \'\' and \'\' will always evaluate to false.', + 175, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with 1 and 1 will always evaluate to true.', + 191, + ], + [ + 'Call to method ImpossibleMethodCall\Foo::isNotSame() with 2 and 2 will always evaluate to false.', + 194, + ], ]); } @@ -264,4 +186,11 @@ public function testReportPhpDoc(): void ]); } + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/impossible-check-type-method-call.neon', + ]; + } + } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php index 64079703c4..9115a5f9ce 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php @@ -2,29 +2,28 @@ namespace PHPStan\Rules\Comparison; -use PHPStan\Tests\AssertionClassStaticMethodTypeSpecifyingExtension; -use PHPStan\Type\PHPUnit\Assert\AssertStaticMethodTypeSpecifyingExtension; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ImpossibleCheckTypeStaticMethodCallRuleTest extends \PHPStan\Testing\RuleTestCase +class ImpossibleCheckTypeStaticMethodCallRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $treatPhpDocTypesAsCertain; - public function getRule(): \PHPStan\Rules\Rule + public function getRule(): Rule { return new ImpossibleCheckTypeStaticMethodCallRule( new ImpossibleCheckTypeHelper( $this->createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), true, - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ); } @@ -33,17 +32,6 @@ protected function shouldTreatPhpDocTypesAsCertain(): bool return $this->treatPhpDocTypesAsCertain; } - /** - * @return \PHPStan\Type\StaticMethodTypeSpecifyingExtension[] - */ - protected function getStaticMethodTypeSpecifyingExtensions(): array - { - return [ - new AssertionClassStaticMethodTypeSpecifyingExtension(null), - new AssertStaticMethodTypeSpecifyingExtension(), - ]; - } - public function testRule(): void { $this->treatPhpDocTypesAsCertain = true; @@ -110,4 +98,11 @@ public function testReportPhpDocs(): void ]); } + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/impossible-check-type-static-method-call.neon', + ]; + } + } diff --git a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php index 068306875b..2e6618f5b0 100644 --- a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -123,4 +124,47 @@ public function testBug4857(): void ]); } + public function testBug5454(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + $this->analyse([__DIR__ . '/data/bug-5454.php'], []); + } + + public function testEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/match-enums.php'], [ + [ + 'Match expression does not handle remaining values: MatchEnums\Foo::THREE|MatchEnums\Foo::TWO', + 19, + ], + [ + 'Match expression does not handle remaining values: MatchEnums\Foo::THREE|MatchEnums\Foo::TWO', + 35, + ], + [ + 'Match expression does not handle remaining value: MatchEnums\Foo::THREE', + 56, + ], + [ + 'Match arm comparison between *NEVER* and MatchEnums\Foo is always false.', + 77, + ], + ]); + } + + public function testBug6394(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-6394.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php index 7d2b12664a..6f56fdcccd 100644 --- a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php @@ -4,9 +4,10 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class NumberComparisonOperatorsConstantConditionRuleTest extends RuleTestCase { @@ -52,4 +53,52 @@ public function testBug5161(): void $this->analyse([__DIR__ . '/data/bug-5161.php'], []); } + public function testBug3310(): void + { + $this->analyse([__DIR__ . '/data/bug-3310.php'], []); + } + + public function testBug3264(): void + { + $this->analyse([__DIR__ . '/data/bug-3264.php'], []); + } + + public function testBug5656(): void + { + $this->analyse([__DIR__ . '/data/bug-5656.php'], []); + } + + public function testBug3867(): void + { + $this->analyse([__DIR__ . '/data/bug-3867.php'], []); + } + + public function testIntegerRangeGeneralization(): void + { + $this->analyse([__DIR__ . '/data/integer-range-generalization.php'], []); + } + + public function testBug3153(): void + { + $this->analyse([__DIR__ . '/data/bug-3153.php'], []); + } + + public function testBug5707(): void + { + $this->analyse([__DIR__ . '/data/bug-5707.php'], []); + } + + public function testBug5969(): void + { + if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + $this->analyse([__DIR__ . '/data/bug-5969.php'], []); + } + + public function testBug5295(): void + { + $this->analyse([__DIR__ . '/data/bug-5295.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index 2b571f01e8..ee5d2dcc42 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -2,16 +2,20 @@ namespace PHPStan\Rules\Comparison; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_INT_SIZE; +use const PHP_VERSION_ID; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class StrictComparisonOfDifferentTypesRuleTest extends \PHPStan\Testing\RuleTestCase +class StrictComparisonOfDifferentTypesRuleTest extends RuleTestCase { - /** @var bool */ - private $checkAlwaysTrueStrictComparison; + private bool $checkAlwaysTrueStrictComparison; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new StrictComparisonOfDifferentTypesRule($this->checkAlwaysTrueStrictComparison); } @@ -83,7 +87,7 @@ public function testStrictComparison(): void 130, ], [ - 'Strict comparison using === between array&nonEmpty and null will always evaluate to false.', + 'Strict comparison using === between non-empty-array and null will always evaluate to false.', 140, ], [ @@ -91,7 +95,7 @@ public function testStrictComparison(): void 154, ], [ - 'Strict comparison using === between array&nonEmpty and null will always evaluate to false.', + 'Strict comparison using === between non-empty-array and null will always evaluate to false.', 164, ], [ @@ -111,11 +115,11 @@ public function testStrictComparison(): void 284, ], [ - 'Strict comparison using === between array(\'X\' => 1) and array(\'X\' => 2) will always evaluate to false.', + 'Strict comparison using === between array{X: 1} and array{X: 2} will always evaluate to false.', 292, ], [ - 'Strict comparison using === between array(\'X\' => 1, \'Y\' => 2) and array(\'X\' => 2, \'Y\' => 1) will always evaluate to false.', + 'Strict comparison using === between array{X: 1, Y: 2} and array{X: 2, Y: 1} will always evaluate to false.', 300, ], [ @@ -123,19 +127,19 @@ public function testStrictComparison(): void 320, ], [ - 'Strict comparison using === between int and \'string\' will always evaluate to false.', + 'Strict comparison using === between int<1, max> and \'string\' will always evaluate to false.', 335, ], [ - 'Strict comparison using === between int and \'string\' will always evaluate to false.', + 'Strict comparison using === between int<0, max> and \'string\' will always evaluate to false.', 343, ], [ - 'Strict comparison using === between int and \'string\' will always evaluate to false.', + 'Strict comparison using === between int<0, max> and \'string\' will always evaluate to false.', 360, ], [ - 'Strict comparison using === between int and \'string\' will always evaluate to false.', + 'Strict comparison using === between int<1, max> and \'string\' will always evaluate to false.', 368, ], [ @@ -155,23 +159,23 @@ public function testStrictComparison(): void 426, ], [ - 'Strict comparison using === between int and null will always evaluate to false.', // todo remove with isDeterministic - 438, - ], - [ - 'Strict comparison using === between int|int<2, max>|string and 1.0 will always evaluate to false.', + 'Strict comparison using === between (int|int<2, max>|string) and 1.0 will always evaluate to false.', 464, ], [ - 'Strict comparison using === between int|int<2, max>|string and stdClass will always evaluate to false.', + 'Strict comparison using === between (int|int<2, max>|string) and stdClass will always evaluate to false.', 466, ], [ - 'Strict comparison using === between int and \'foo\' will always evaluate to false.', + 'Strict comparison using === between int<0, 1> and 100 will always evaluate to false.', + 622, + ], + [ + 'Strict comparison using === between 100 and \'foo\' will always evaluate to false.', 624, ], [ - 'Strict comparison using === between int and \'foo\' will always evaluate to false.', + 'Strict comparison using === between int<10, max> and \'foo\' will always evaluate to false.', 635, ], [ @@ -226,7 +230,7 @@ public function testStrictComparison(): void 'Strict comparison using === between 1000 and 1000 will always evaluate to true.', 910, ], - ] + ], ); } @@ -277,11 +281,11 @@ public function testStrictComparisonWithoutAlwaysTrue(): void 98, ], [ - 'Strict comparison using === between array&nonEmpty and null will always evaluate to false.', + 'Strict comparison using === between non-empty-array and null will always evaluate to false.', 140, ], [ - 'Strict comparison using === between array&nonEmpty and null will always evaluate to false.', + 'Strict comparison using === between non-empty-array and null will always evaluate to false.', 164, ], [ @@ -289,11 +293,11 @@ public function testStrictComparisonWithoutAlwaysTrue(): void 284, ], [ - 'Strict comparison using === between array(\'X\' => 1) and array(\'X\' => 2) will always evaluate to false.', + 'Strict comparison using === between array{X: 1} and array{X: 2} will always evaluate to false.', 292, ], [ - 'Strict comparison using === between array(\'X\' => 1, \'Y\' => 2) and array(\'X\' => 2, \'Y\' => 1) will always evaluate to false.', + 'Strict comparison using === between array{X: 1, Y: 2} and array{X: 2, Y: 1} will always evaluate to false.', 300, ], [ @@ -301,19 +305,19 @@ public function testStrictComparisonWithoutAlwaysTrue(): void 320, ], [ - 'Strict comparison using === between int and \'string\' will always evaluate to false.', + 'Strict comparison using === between int<1, max> and \'string\' will always evaluate to false.', 335, ], [ - 'Strict comparison using === between int and \'string\' will always evaluate to false.', + 'Strict comparison using === between int<0, max> and \'string\' will always evaluate to false.', 343, ], [ - 'Strict comparison using === between int and \'string\' will always evaluate to false.', + 'Strict comparison using === between int<0, max> and \'string\' will always evaluate to false.', 360, ], [ - 'Strict comparison using === between int and \'string\' will always evaluate to false.', + 'Strict comparison using === between int<1, max> and \'string\' will always evaluate to false.', 368, ], [ @@ -329,23 +333,23 @@ public function testStrictComparisonWithoutAlwaysTrue(): void 408, ], [ - 'Strict comparison using === between int and null will always evaluate to false.', // todo remove with isDeterministic - 438, - ], - [ - 'Strict comparison using === between int|int<2, max>|string and 1.0 will always evaluate to false.', + 'Strict comparison using === between (int|int<2, max>|string) and 1.0 will always evaluate to false.', 464, ], [ - 'Strict comparison using === between int|int<2, max>|string and stdClass will always evaluate to false.', + 'Strict comparison using === between (int|int<2, max>|string) and stdClass will always evaluate to false.', 466, ], [ - 'Strict comparison using === between int and \'foo\' will always evaluate to false.', + 'Strict comparison using === between int<0, 1> and 100 will always evaluate to false.', + 622, + ], + [ + 'Strict comparison using === between 100 and \'foo\' will always evaluate to false.', 624, ], [ - 'Strict comparison using === between int and \'foo\' will always evaluate to false.', + 'Strict comparison using === between int<10, max> and \'foo\' will always evaluate to false.', 635, ], [ @@ -364,7 +368,7 @@ public function testStrictComparisonWithoutAlwaysTrue(): void 'Strict comparison using === between mixed and \'foo\' will always evaluate to false.', 808, ], - ] + ], ); } @@ -456,4 +460,47 @@ public function testBug3357(): void $this->analyse([__DIR__ . '/data/bug-3357.php'], []); } + public function testBug4848(): void + { + if (PHP_INT_SIZE !== 8) { + $this->markTestSkipped('Test requires 64-bit platform.'); + } + $this->checkAlwaysTrueStrictComparison = true; + $this->analyse([__DIR__ . '/data/bug-4848.php'], [ + [ + 'Strict comparison using === between \'18446744073709551615\' and \'9223372036854775807\' will always evaluate to false.', + 7, + ], + ]); + } + + public function testBug4793(): void + { + $this->checkAlwaysTrueStrictComparison = true; + $this->analyse([__DIR__ . '/data/bug-4793.php'], []); + } + + public function testBug5062(): void + { + $this->checkAlwaysTrueStrictComparison = true; + $this->analyse([__DIR__ . '/data/bug-5062.php'], []); + } + + public function testBug3366(): void + { + $this->checkAlwaysTrueStrictComparison = true; + $this->analyse([__DIR__ . '/data/bug-3366.php'], []); + } + + public function testBug5362(): void + { + $this->checkAlwaysTrueStrictComparison = true; + $this->analyse([__DIR__ . '/data/bug-5362.php'], [ + [ + 'Strict comparison using === between 0 and 1|2 will always evaluate to false.', + 23, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php index d68bf08601..1403768f98 100644 --- a/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php @@ -2,16 +2,18 @@ namespace PHPStan\Rules\Comparison; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class TernaryOperatorConstantConditionRuleTest extends \PHPStan\Testing\RuleTestCase +class TernaryOperatorConstantConditionRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $treatPhpDocTypesAsCertain; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new TernaryOperatorConstantConditionRule( new ConstantConditionRuleHelper( @@ -19,11 +21,11 @@ protected function getRule(): \PHPStan\Rules\Rule $this->createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ); } diff --git a/tests/PHPStan/Rules/Comparison/UnreachableIfBranchesRuleTest.php b/tests/PHPStan/Rules/Comparison/UnreachableIfBranchesRuleTest.php index 66034184c4..a465f71128 100644 --- a/tests/PHPStan/Rules/Comparison/UnreachableIfBranchesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/UnreachableIfBranchesRuleTest.php @@ -6,13 +6,12 @@ use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class UnreachableIfBranchesRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $treatPhpDocTypesAsCertain; protected function getRule(): Rule { @@ -22,11 +21,11 @@ protected function getRule(): Rule $this->createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ); } diff --git a/tests/PHPStan/Rules/Comparison/UnreachableTernaryElseBranchRuleTest.php b/tests/PHPStan/Rules/Comparison/UnreachableTernaryElseBranchRuleTest.php index 93c7d52bcc..abfca34a8f 100644 --- a/tests/PHPStan/Rules/Comparison/UnreachableTernaryElseBranchRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/UnreachableTernaryElseBranchRuleTest.php @@ -6,13 +6,12 @@ use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class UnreachableTernaryElseBranchRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $treatPhpDocTypesAsCertain; protected function getRule(): Rule { @@ -22,11 +21,11 @@ protected function getRule(): Rule $this->createReflectionProvider(), $this->getTypeSpecifier(), [], - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ), - $this->treatPhpDocTypesAsCertain + $this->treatPhpDocTypesAsCertain, ); } diff --git a/tests/PHPStan/Rules/Comparison/UsageOfVoidMatchExpressionRuleTest.php b/tests/PHPStan/Rules/Comparison/UsageOfVoidMatchExpressionRuleTest.php index bf873a965c..f75ea691dc 100644 --- a/tests/PHPStan/Rules/Comparison/UsageOfVoidMatchExpressionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/UsageOfVoidMatchExpressionRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase diff --git a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php new file mode 100644 index 0000000000..a85dae7241 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php @@ -0,0 +1,52 @@ + + */ +class WhileLoopAlwaysFalseConditionRuleTest extends RuleTestCase +{ + + private bool $treatPhpDocTypesAsCertain = true; + + protected function getRule(): Rule + { + return new WhileLoopAlwaysFalseConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + $this->createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), + $this->treatPhpDocTypesAsCertain, + ), + $this->treatPhpDocTypesAsCertain, + ); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return $this->treatPhpDocTypesAsCertain; + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/while-loop-false.php'], [ + [ + 'While loop condition is always false.', + 10, + ], + [ + 'While loop condition is always false.', + 20, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php new file mode 100644 index 0000000000..54c9c1be40 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php @@ -0,0 +1,56 @@ + + */ +class WhileLoopAlwaysTrueConditionRuleTest extends RuleTestCase +{ + + private bool $treatPhpDocTypesAsCertain = true; + + protected function getRule(): Rule + { + return new WhileLoopAlwaysTrueConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + $this->createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), + $this->treatPhpDocTypesAsCertain, + ), + $this->treatPhpDocTypesAsCertain, + ); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return $this->treatPhpDocTypesAsCertain; + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/while-loop-true.php'], [ + [ + 'While loop condition is always true.', + 10, + ], + [ + 'While loop condition is always true.', + 20, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + [ + 'While loop condition is always true.', + 65, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/TestMethodTypeSpecifyingExtensions.php b/tests/PHPStan/Rules/Comparison/data/TestMethodTypeSpecifyingExtensions.php new file mode 100644 index 0000000000..258f5251a9 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/TestMethodTypeSpecifyingExtensions.php @@ -0,0 +1,241 @@ +typeSpecifier = $typeSpecifier; + } + + public function getClass(): string + { + return \PHPStan\Tests\AssertionClass::class; + } + + public function isMethodSupported( + MethodReflection $methodReflection, + MethodCall $node, + TypeSpecifierContext $context + ): bool + { + return $methodReflection->getName() === 'assertNotInt' + && count($node->args) > 0; + } + + public function specifyTypes( + MethodReflection $methodReflection, + MethodCall $node, + Scope $scope, + TypeSpecifierContext $context + ): SpecifiedTypes + { + return $this->typeSpecifier->specifyTypesInCondition( + $scope, + new \PhpParser\Node\Expr\BooleanNot( + new \PhpParser\Node\Expr\FuncCall( + new \PhpParser\Node\Name('is_int'), + [ + $node->args[0], + ] + ) + ), + TypeSpecifierContext::createTruthy() + ); + } + +} + +class FooIsSame implements MethodTypeSpecifyingExtension, TypeSpecifierAwareExtension +{ + + /** @var TypeSpecifier */ + private $typeSpecifier; + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + + public function getClass(): string + { + return \ImpossibleMethodCall\Foo::class; + } + + public function isMethodSupported( + MethodReflection $methodReflection, + MethodCall $node, + TypeSpecifierContext $context + ): bool + { + return $methodReflection->getName() === 'isSame' + && count($node->args) >= 2; + } + + public function specifyTypes( + MethodReflection $methodReflection, + MethodCall $node, + Scope $scope, + TypeSpecifierContext $context + ): SpecifiedTypes + { + return $this->typeSpecifier->specifyTypesInCondition( + $scope, + new \PhpParser\Node\Expr\BinaryOp\Identical( + $node->args[0]->value, + $node->args[1]->value + ), + $context + ); + } + +} + +class FooIsNotSame implements MethodTypeSpecifyingExtension, + TypeSpecifierAwareExtension { + + /** @var TypeSpecifier */ + private $typeSpecifier; + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + + public function getClass(): string + { + return \ImpossibleMethodCall\Foo::class; + } + + public function isMethodSupported( + MethodReflection $methodReflection, + MethodCall $node, + TypeSpecifierContext $context + ): bool + { + return $methodReflection->getName() === 'isNotSame' + && count($node->args) >= 2; + } + + public function specifyTypes( + MethodReflection $methodReflection, + MethodCall $node, + Scope $scope, + TypeSpecifierContext $context + ): SpecifiedTypes + { + return $this->typeSpecifier->specifyTypesInCondition( + $scope, + new \PhpParser\Node\Expr\BinaryOp\NotIdentical( + $node->args[0]->value, + $node->args[1]->value + ), + $context + ); + } + +} + +class FooIsEqual implements MethodTypeSpecifyingExtension, TypeSpecifierAwareExtension +{ + + /** @var TypeSpecifier */ + private $typeSpecifier; + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + + public function getClass(): string + { + return \ImpossibleMethodCall\Foo::class; + } + + public function isMethodSupported( + MethodReflection $methodReflection, + MethodCall $node, + TypeSpecifierContext $context + ): bool + { + return $methodReflection->getName() === 'isSame' + && count($node->args) >= 2; + } + + public function specifyTypes( + MethodReflection $methodReflection, + MethodCall $node, + Scope $scope, + TypeSpecifierContext $context + ): SpecifiedTypes + { + return $this->typeSpecifier->specifyTypesInCondition( + $scope, + new \PhpParser\Node\Expr\BinaryOp\Equal( + $node->args[0]->value, + $node->args[1]->value + ), + $context + ); + } + +} + +class FooIsNotEqual implements MethodTypeSpecifyingExtension, + TypeSpecifierAwareExtension { + + /** @var TypeSpecifier */ + private $typeSpecifier; + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + + public function getClass(): string + { + return \ImpossibleMethodCall\Foo::class; + } + + public function isMethodSupported( + MethodReflection $methodReflection, + MethodCall $node, + TypeSpecifierContext $context + ): bool + { + return $methodReflection->getName() === 'isNotSame' + && count($node->args) >= 2; + } + + public function specifyTypes( + MethodReflection $methodReflection, + MethodCall $node, + Scope $scope, + TypeSpecifierContext $context + ): SpecifiedTypes + { + return $this->typeSpecifier->specifyTypesInCondition( + $scope, + new \PhpParser\Node\Expr\BinaryOp\NotEqual( + $node->args[0]->value, + $node->args[1]->value + ), + $context + ); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/TestTypeOverwriteSpecifyingExtensions.php b/tests/PHPStan/Rules/Comparison/data/TestTypeOverwriteSpecifyingExtensions.php new file mode 100644 index 0000000000..4e2acbd198 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/TestTypeOverwriteSpecifyingExtensions.php @@ -0,0 +1,58 @@ +typeSpecifier = $typeSpecifier; + } + + public function getClass(): string + { + return \GenericTypeOverride\Foo::class; + } + + public function isMethodSupported( + MethodReflection $methodReflection, + MethodCall $node, + TypeSpecifierContext $context + ): bool + { + return $methodReflection->getName() === 'setFetchMode'; + } + + public function specifyTypes( + MethodReflection $methodReflection, + MethodCall $node, + Scope $scope, + TypeSpecifierContext $context + ): SpecifiedTypes + { + $newType = new GenericObjectType(\GenericTypeOverride\Foo::class, [new ObjectType(\GenericTypeOverride\Bar::class)]); + + return $this->typeSpecifier->create( + $node->var, + $newType, + TypeSpecifierContext::createTruthy(), + true + ); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/array-is-list.php b/tests/PHPStan/Rules/Comparison/data/array-is-list.php new file mode 100644 index 0000000000..f812dc9a06 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/array-is-list.php @@ -0,0 +1,48 @@ + $stringKeyedArray + */ + public function doFoo(array $stringKeyedArray) + { + if (array_is_list($stringKeyedArray)) { + + } + } + + /** + * @param array $mixedArray + */ + public function doBar(array $mixedArray) + { + if (array_is_list($mixedArray)) { + // Fine + } + } + + /** + * @param array $arrayKeyedInts + */ + public function doBaz(array $arrayKeyedInts) + { + if (array_is_list($arrayKeyedInts)) { + // Fine + } + } + + public function doBax() + { + if (array_is_list(['foo' => 'bar', 'bar' => 'baz'])) { + + } + + if (array_is_list(['foo', 'foo' => 'bar', 'bar' => 'baz'])) { + // Fine + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-1746.php b/tests/PHPStan/Rules/Comparison/data/bug-1746.php new file mode 100644 index 0000000000..c6b36ee3a7 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-1746.php @@ -0,0 +1,30 @@ + ['blah' => 'boohoo']]; + $assocModel = 'foo'; + $parents = ['Class' => ['foo' => 'bar', 'bar' => 'baz', 'foreignKey' => 'blah']]; + + // initial value + $isMatch = true; + foreach ($parents as $parentModel) { + $fk = $parentModel['foreignKey']; + if (isset($data[$fk])) { + // redetermine whether $isMatch is still true + $isMatch = $isMatch && ($data[$fk] == $existing[$assocModel][$fk]); + + // bail + if (!$isMatch) { + break; + } + } + } + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-2870.php b/tests/PHPStan/Rules/Comparison/data/bug-2870.php new file mode 100644 index 0000000000..79d9d76cfc --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-2870.php @@ -0,0 +1,9 @@ + 10 ){ + break; + } + } + } + } + + public function doBar() + { + $rows = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]; + $added_rows = 0; + $limit = random_int(1, 20); + + foreach($rows as $row){ + + if( $added_rows >= $limit ){ + break; + } + $added_rows++; + } + + if( $added_rows < 3 ){ + foreach($rows as $row){ + + $added_rows++; + + if( $added_rows > 10 ){ + break; + } + } + } + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-3264.php b/tests/PHPStan/Rules/Comparison/data/bug-3264.php new file mode 100644 index 0000000000..34fb93f0f2 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-3264.php @@ -0,0 +1,17 @@ + + */ + function get_foo(): array + { + return []; + } + + public function doFoo(): void + { + $foo = $this->get_foo(); + for ($i = 0; $i < \count($foo); $i++) { + if (\array_key_exists($i + 1, $foo) + && \array_key_exists($i + 2, $foo) + ) { + echo $i; + } + } + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-3867.php b/tests/PHPStan/Rules/Comparison/data/bug-3867.php new file mode 100644 index 0000000000..46bf02e5f4 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-3867.php @@ -0,0 +1,24 @@ +$method(); + + if (!empty($result)) { + break; + } + } while (count($try) > 0); + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-3892.php b/tests/PHPStan/Rules/Comparison/data/bug-3892.php new file mode 100644 index 0000000000..8148f7533a --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-3892.php @@ -0,0 +1,72 @@ + '200', ReceiptOrder::class => '300']; + + /** @var Order[] */ + private $dtos; + + public function __construct(OrderEntity $order) + { + $this->dtos = \array_filter([ReceiptOrder::fromOrder($order), PickingOrder::fromOrder($order)]); + } + + + /** + * @return Order[] + */ + public function getDTOs(): array + { + return $this->dtos; + } +} + +abstract class Order +{ + public const TYP = [PickingOrder::class => '200', ReceiptOrder::class => '300']; +} + +class PickingOrder extends Order +{ + public static function fromOrder(OrderEntity $order): ?self + { + return $order->isLoaded() ? new self() : null; + } +} + +class ReceiptOrder extends Order +{ + public static function fromOrder(OrderEntity $order): ?self + { + return $order->isLoaded() ? new self() : null; + } +} + +class Foo +{ + + public function doFoo(OrderSaved $event) + { + $DTOs = $event->getDTOs(); + + $DTOClasses = \array_map('\get_class', $DTOs); + $missingClasses = \array_diff(\array_keys(Order::TYP), $DTOClasses); + + if (\in_array(ReceiptOrder::class, $missingClasses, true)) { + + } + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-4666.php b/tests/PHPStan/Rules/Comparison/data/bug-4666.php new file mode 100644 index 0000000000..b9e67f0a50 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-4666.php @@ -0,0 +1,29 @@ + $objects */ + public function test(array $objects): void + { + $types = []; + foreach ($objects as $object) { + if (self::CONST_1 === $object->getType() && !in_array(self::CONST_2, $types, true)) { + $types[] = self::CONST_2; + } + } + } +} + +class MyObject +{ + /** @var string */ + private $type; + public function getType(): string{ + return $this->type; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-4793.php b/tests/PHPStan/Rules/Comparison/data/bug-4793.php new file mode 100644 index 0000000000..c0d043061a --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-4793.php @@ -0,0 +1,41 @@ +getValue() as $key => $val) { + if ($key >= 5 && $key <= 10) { + } elseif ($key > 10 && $key <= 15) { + } else { + } + } + } + + /** + * @return array + */ + public function getValue(): array { + return []; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-5354.php b/tests/PHPStan/Rules/Comparison/data/bug-5354.php new file mode 100644 index 0000000000..6d8571af09 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-5354.php @@ -0,0 +1,21 @@ + 10 ? 0 : 1; + } + + if (\in_array(0, $a, true)) { + return; + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-5362.php b/tests/PHPStan/Rules/Comparison/data/bug-5362.php new file mode 100644 index 0000000000..51284284ca --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-5362.php @@ -0,0 +1,32 @@ +doFoo($retry); + + break; + } catch (\Exception $e) { + if (0 === $retry) { + throw $e; + } + + --$retry; + } + } while ($retry > 0); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-5369.php b/tests/PHPStan/Rules/Comparison/data/bug-5369.php new file mode 100644 index 0000000000..84707aa8ad --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-5369.php @@ -0,0 +1,23 @@ += 8.0 + +namespace Bug5454; + +class TextFormat{ + public const ESCAPE = "\xc2\xa7"; //§ + + public const BLACK = TextFormat::ESCAPE . "0"; + public const DARK_BLUE = TextFormat::ESCAPE . "1"; + public const DARK_GREEN = TextFormat::ESCAPE . "2"; + public const DARK_AQUA = TextFormat::ESCAPE . "3"; + public const DARK_RED = TextFormat::ESCAPE . "4"; + public const DARK_PURPLE = TextFormat::ESCAPE . "5"; + + public const OBFUSCATED = TextFormat::ESCAPE . "k"; + public const BOLD = TextFormat::ESCAPE . "l"; + public const STRIKETHROUGH = TextFormat::ESCAPE . "m"; + public const UNDERLINE = TextFormat::ESCAPE . "n"; + public const ITALIC = TextFormat::ESCAPE . "o"; + public const RESET = TextFormat::ESCAPE . "r"; +} + +class Terminal +{ + + /** + * @param string[] $string + */ + public static function toANSI(array $string) : string{ + $newString = ""; + foreach($string as $token){ + $newString .= match ($token){ + TextFormat::BOLD => "bold", + TextFormat::OBFUSCATED => "obf", + TextFormat::ITALIC => "italic", + TextFormat::UNDERLINE => "underline", + TextFormat::STRIKETHROUGH => "strike", + TextFormat::RESET => "reset", + TextFormat::BLACK => "black", + TextFormat::DARK_BLUE => "blue", + TextFormat::DARK_GREEN => "green", + TextFormat::DARK_AQUA => "aqua", + TextFormat::DARK_RED => "red", + default => $token, + }; + } + + return $newString; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-5496.php b/tests/PHPStan/Rules/Comparison/data/bug-5496.php new file mode 100644 index 0000000000..dc5f9c6f77 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-5496.php @@ -0,0 +1,32 @@ + $propagation + */ + public function propagate($propagation): void + { + } +} + +class Foo +{ + + public function doFoo() + { + $type = new ConstParamTypes(); + + /** @var array $propagation */ + $propagation = []; + + if (\in_array('auto', $propagation, true)) { + $type->propagate($propagation); + } + + $type->propagate(['yakdam' => 'copy']); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-5656.php b/tests/PHPStan/Rules/Comparison/data/bug-5656.php new file mode 100644 index 0000000000..aca016dfbb --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-5656.php @@ -0,0 +1,41 @@ + 10) { + $i = 1; + } + $control += $i * $v; + ++$i; + } + + $control %= 11; + + if (10 !== $control) { + break; + } + } + + if (10 === $control) { + $control = 0; + } + + return $expected === $control; +} + +$values = [0, 1, 0, 6, 0, 6, 2, 3, 1, 5]; +okpoValidate($values); diff --git a/tests/PHPStan/Rules/Comparison/data/bug-5707.php b/tests/PHPStan/Rules/Comparison/data/bug-5707.php new file mode 100644 index 0000000000..3a93703d60 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-5707.php @@ -0,0 +1,16 @@ + 0; --$l) { + } + + return 'x'; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-5969.php b/tests/PHPStan/Rules/Comparison/data/bug-5969.php new file mode 100644 index 0000000000..87693235ad --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-5969.php @@ -0,0 +1,20 @@ += 7.4 + +namespace Bug5969; + +class HelloWorld +{ + public string $date; + + public function sayHello(object $o): void + { + if (! isset($o->date)) return; + + if (strtotime($o->date) < time()) echo "a"; + + // surprisingly this is not an issue + if (strtotime($this->date) < time()) echo "b"; + + if (is_string($o->date) && strtotime($o->date) < time()) echo "c"; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6258.php b/tests/PHPStan/Rules/Comparison/data/bug-6258.php new file mode 100644 index 0000000000..607c8650ec --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6258.php @@ -0,0 +1,17 @@ += 8.0 + +namespace Bug6258; + +defined('a') || die(); +defined('a') or die(); +rand() === rand() || die(); + + +defined('a') || exit(); +defined('a') or exit(); +rand() === rand() || exit(); + + +defined('a') || throw new \Exception(''); +defined('a') or throw new \Exception(''); +rand() === rand() || throw new \Exception(''); diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6305.php b/tests/PHPStan/Rules/Comparison/data/bug-6305.php new file mode 100644 index 0000000000..c4d142a276 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6305.php @@ -0,0 +1,15 @@ += 8.1 + +namespace Bug6394; + +enum EntryType: string +{ + case CREDIT = 'credit'; + case DEBIT = 'debit'; +} + +class Foo +{ + + public function getType(): EntryType + { + return $this->type; + } + + public function getAmount(): int + { + return match($this->getType()) { + EntryType::DEBIT => 1, + EntryType::CREDIT => 2, + }; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6473.php b/tests/PHPStan/Rules/Comparison/data/bug-6473.php new file mode 100644 index 0000000000..9482373e9d --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6473.php @@ -0,0 +1,48 @@ += 7.4 + +namespace Bug6473; + +use function PHPStan\Testing\assertType; + +class Point { + public bool $visited = false; + + /** + * @return Point[] + */ + public function getNeighbours(): array { + return [ new Point ]; + } + + public function doFoo() + { + $seen = []; + + foreach([new Point, new Point] as $p ) { + + $p->visited = true; + assertType('true', $p->visited); + $seen = [ + ... $seen, + ... array_filter( $p->getNeighbours(), static fn (Point $p) => !$p->visited ) + ]; + assertType('true', $p->visited); + } + } + + public function doFoo2() + { + $seen = []; + + foreach([new Point, new Point] as $p ) { + + $p->visited = true; + assertType('true', $p->visited); + $seen = [ + ... $seen, + ... array_filter( $p->getNeighbours(), static fn (Point $p2) => !$p2->visited ) + ]; + assertType('true', $p->visited); + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6698.php b/tests/PHPStan/Rules/Comparison/data/bug-6698.php new file mode 100644 index 0000000000..c65e7e977a --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6698.php @@ -0,0 +1,31 @@ + + */ + public function getClasses(): iterable; +} + +class Y +{ + /** @var X */ + public $x; + + /** + * @template T of object + * + * @param class-string $type + * @return iterable> + */ + public function findImplementations(string $type): iterable + { + foreach ($this->x->getClasses() as $class) { + if (is_subclass_of($class, $type)) { + yield $class; + } + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/do-while-loop.php b/tests/PHPStan/Rules/Comparison/data/do-while-loop.php new file mode 100644 index 0000000000..02eb303d34 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/do-while-loop.php @@ -0,0 +1,98 @@ +createGenericFoo(); + assertType('Foo', $foo); + + // $foo generic will be overridden via MethodTypeSpecifyingExtension + $foo->setFetchMode(); + assertType('Foo', $foo); + } + + /** + * @return Foo + */ + public function createGenericFoo() { + + } +} + + +/** + * @template T + */ +class Foo +{ + public function setFetchMode() { + + } +} + + +class Bar +{ +} diff --git a/tests/PHPStan/Rules/Comparison/data/if-condition.php b/tests/PHPStan/Rules/Comparison/data/if-condition.php index 54887dc4de..2898fb5acb 100644 --- a/tests/PHPStan/Rules/Comparison/data/if-condition.php +++ b/tests/PHPStan/Rules/Comparison/data/if-condition.php @@ -293,3 +293,20 @@ public function nestedIfConditions($mixed): void } } } + +class EmptyCondition +{ + + public function doFoo() + { + $a = 1; + if (empty($a)) { + + } + + if (empty($b)) { + + } + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/impossible-method-call.php b/tests/PHPStan/Rules/Comparison/data/impossible-method-call.php index c213bfb8d5..b7e1ab7905 100644 --- a/tests/PHPStan/Rules/Comparison/data/impossible-method-call.php +++ b/tests/PHPStan/Rules/Comparison/data/impossible-method-call.php @@ -92,6 +92,62 @@ public function doDolor(\stdClass $std1, \stdClass $std2) } } + if ($this->isSame(self::createStdClass('a'), self::createStdClass('a'))) { + + } + if ($this->isNotSame(self::createStdClass('b'), self::createStdClass('b'))) { + + } + if ($this->isSame(self::returnFoo('a'), self::returnFoo('a'))) { + + } + if ($this->isNotSame(self::returnFoo('b'), self::returnFoo('b'))) { + + } + if ($this->isSame(self::createStdClass('a')->foo, self::createStdClass('a')->foo)) { + + } + if ($this->isNotSame(self::createStdClass('b')->foo, self::createStdClass('b')->foo)) { + + } + if ($this->isSame([], [])) { + + } + if ($this->isNotSame([], [])) { + + } + if ($this->isSame([1, 3], [1, 3])) { + + } + if ($this->isNotSame([1, 3], [1, 3])) { + + } + $std3 = new \stdClass(); + if ($this->isSame(1, $std3)) { + + } + $std4 = new \stdClass(); + if ($this->isNotSame(1, $std4)) { + + } + if ($this->isSame('1', new \stdClass())) { + + } + if ($this->isNotSame('1', new \stdClass())) { + + } + if ($this->isSame(['a', 'b'], [1, 2])) { + + } + if ($this->isNotSame(['a', 'b'], [1, 2])) { + + } + if ($this->isSame(new \stdClass(), '1')) { + + } + if ($this->isNotSame(new \stdClass(), '1')) { + + } } public function nullableInt(): ?int @@ -99,4 +155,43 @@ public function nullableInt(): ?int } + public static function createStdClass(string $foo): \stdClass + { + return new \stdClass(); + } + + /** + * @return 'foo' + */ + public static function returnFoo(string $foo): string + { + return 'foo'; + } + + public function nonEmptyString() + { + $s = ''; + $this->isSame($s, ''); + $this->isNotSame($s, ''); + } + + public function stdClass(\stdClass $a) + { + $this->isSame($a, new \stdClass()); + } + + public function stdClass2(\stdClass $a) + { + $this->isNotSame($a, new \stdClass()); + } + + public function scalars() + { + $i = 1; + $this->isSame($i, 1); + + $j = 2; + $this->isNotSame($j, 2); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/in-array-date-format.php b/tests/PHPStan/Rules/Comparison/data/in-array-date-format.php new file mode 100644 index 0000000000..f5f888f804 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/in-array-date-format.php @@ -0,0 +1,80 @@ + $v) { + $result[$k] = $dt->format('d'); + } + + $d = new \DateTimeImmutable(); + if (in_array($d->format('d'), $result, true)) { + + } + + if (in_array('01', $result, true)) { + + } + + $day = $d->format('d'); + if (rand(0, 1)) { + $day = '32'; + } + + if (in_array($day, $result, true)) { + + } + } + + /** + * @param non-empty-array $a + */ + public function doBar(array $a, int $i) + { + if (in_array('a', $a, true)) { + + } + + if (in_array('b', $a, true)) { + + } + + if (in_array($i, [], true)) { + + } + } + + /** + * @param array $a + */ + public function doBaz(array $a, int $i, string $s) + { + if (in_array($s, $a, true)) { + + } + + if (in_array($i, $a, true)) { + + } + } + + /** + * @param non-empty-array $a + */ + public function doLorem(array $a, int $i) + { + if (in_array('a', $a, true)) { + + } + + if (in_array('b', $a, true)) { + + } + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/integer-range-generalization.php b/tests/PHPStan/Rules/Comparison/data/integer-range-generalization.php new file mode 100644 index 0000000000..c79e6e2106 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/integer-range-generalization.php @@ -0,0 +1,28 @@ += 8.1 + +namespace MatchEnums; + +enum Foo: int +{ + + case ONE = 1; + case TWO = 2; + case THREE = 3; + + public function returnStatic(): static + { + return $this; + } + + public function doFoo(): string + { + return match ($this->returnStatic()) { + self::ONE => 'one', + }; + } + + public function doBar(): string + { + return match ($this->returnStatic()) { + Foo::ONE => 'one', + Foo::TWO => 'two', + Foo::THREE => 'three', + }; + } + + public function doBaz(): string + { + return match ($this) { + self::ONE => 'one', + }; + } + + public function doIpsum(): string + { + return match ($this) { + Foo::ONE => 'one', + Foo::TWO => 'two', + Foo::THREE => 'three', + }; + } + +} + +class Bar +{ + + public function doFoo(Foo $foo): int + { + return match ($foo) { + Foo::ONE => 'one', + Foo::TWO => 'two', + }; + } + + public function doBar(Foo $foo): int + { + return match ($foo) { + Foo::ONE => 'one', + Foo::TWO => 'two', + Foo::THREE => 'three', + }; + } + + public function doBaz(Foo $foo, Foo $bar): int + { + return match ($foo) { + Foo::ONE => 'one', + Foo::TWO => 'two', + Foo::THREE => 'three', + $bar => 'four', + }; + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/number-comparison-operators.php b/tests/PHPStan/Rules/Comparison/data/number-comparison-operators.php index eb7d0d5c77..09ea18d059 100644 --- a/tests/PHPStan/Rules/Comparison/data/number-comparison-operators.php +++ b/tests/PHPStan/Rules/Comparison/data/number-comparison-operators.php @@ -23,3 +23,11 @@ function (int $i): void { } } }; + +function (): void { + for ($X = -3; $X <= 3; ++$X){ + for ($Z = -3; $Z <= 3; ++$Z){ + + } + } +}; diff --git a/tests/PHPStan/Rules/Comparison/data/strict-comparison.php b/tests/PHPStan/Rules/Comparison/data/strict-comparison.php index 368aab3305..4d2f5c61a4 100644 --- a/tests/PHPStan/Rules/Comparison/data/strict-comparison.php +++ b/tests/PHPStan/Rules/Comparison/data/strict-comparison.php @@ -440,7 +440,7 @@ public function doFoo() } } } - + /** @phpstan-impure */ public function nullableInt(): ?int { @@ -949,3 +949,28 @@ public function test(int $key) } } } + +class ArrayWithNonEmptyStringValue +{ + + /** + * @param array $a + */ + public function doFoo(array $a): void + { + if ($a === []) { + + } + } + + /** + * @param array $a + */ + public function doBar(array $a): void + { + if ($a === []) { + + } + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/while-loop-false.php b/tests/PHPStan/Rules/Comparison/data/while-loop-false.php new file mode 100644 index 0000000000..667156e7c5 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/while-loop-false.php @@ -0,0 +1,35 @@ + + * @extends RuleTestCase */ -class ConstantRuleTest extends \PHPStan\Testing\RuleTestCase +class ConstantRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new ConstantRule(); } diff --git a/tests/PHPStan/Rules/Constants/DirectAlwaysUsedClassConstantsExtensionProvider.php b/tests/PHPStan/Rules/Constants/DirectAlwaysUsedClassConstantsExtensionProvider.php index 924992ae73..861c72d7ab 100644 --- a/tests/PHPStan/Rules/Constants/DirectAlwaysUsedClassConstantsExtensionProvider.php +++ b/tests/PHPStan/Rules/Constants/DirectAlwaysUsedClassConstantsExtensionProvider.php @@ -5,15 +5,11 @@ class DirectAlwaysUsedClassConstantsExtensionProvider implements AlwaysUsedClassConstantsExtensionProvider { - /** @var AlwaysUsedClassConstantsExtension[] */ - private $extensions; - /** * @param AlwaysUsedClassConstantsExtension[] $extensions */ - public function __construct(array $extensions) + public function __construct(private array $extensions) { - $this->extensions = $extensions; } /** diff --git a/tests/PHPStan/Rules/Constants/FinalConstantRuleTest.php b/tests/PHPStan/Rules/Constants/FinalConstantRuleTest.php new file mode 100644 index 0000000000..1dc01002f8 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/FinalConstantRuleTest.php @@ -0,0 +1,55 @@ + + */ +class FinalConstantRuleTest extends RuleTestCase +{ + + private int $phpVersionId; + + protected function getRule(): Rule + { + return new FinalConstantRule(new PhpVersion($this->phpVersionId)); + } + + public function dataRule(): array + { + return [ + [ + 80000, + [ + [ + 'Final class constants are supported only on PHP 8.1 and later.', + 9, + ], + ], + ], + [ + 80100, + [], + ], + ]; + } + + /** + * @dataProvider dataRule + * @param mixed[] $errors + */ + public function testRule(int $phpVersionId, array $errors): void + { + if (!self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires static reflection.'); + } + + $this->phpVersionId = $phpVersionId; + $this->analyse([__DIR__ . '/data/final-constant.php'], $errors); + } + +} diff --git a/tests/PHPStan/Rules/Constants/MissingClassConstantTypehintRuleTest.php b/tests/PHPStan/Rules/Constants/MissingClassConstantTypehintRuleTest.php new file mode 100644 index 0000000000..5d5de8e71a --- /dev/null +++ b/tests/PHPStan/Rules/Constants/MissingClassConstantTypehintRuleTest.php @@ -0,0 +1,41 @@ + + */ +class MissingClassConstantTypehintRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + return new MissingClassConstantTypehintRule(new MissingTypehintCheck($reflectionProvider, true, true, true, [])); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/missing-class-constant-typehint.php'], [ + [ + 'Constant MissingClassConstantTypehint\Foo::BAR type has no value type specified in iterable type array.', + 11, + 'See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type', + ], + [ + 'Constant MissingClassConstantTypehint\Foo::BAZ with generic class MissingClassConstantTypehint\Bar does not specify its types: T', + 17, + 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', + ], + [ + 'Constant MissingClassConstantTypehint\Foo::LOREM type has no signature specified for callable.', + 20, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Constants/OverridingConstantRuleTest.php b/tests/PHPStan/Rules/Constants/OverridingConstantRuleTest.php new file mode 100644 index 0000000000..a4c5d337a4 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/OverridingConstantRuleTest.php @@ -0,0 +1,97 @@ + */ +class OverridingConstantRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new OverridingConstantRule(true); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/overriding-constant.php'], [ + [ + 'Type string of constant OverridingConstant\Bar::BAR is not covariant with type int of constant OverridingConstant\Foo::BAR.', + 30, + ], + [ + 'Type int|string of constant OverridingConstant\Bar::IPSUM is not covariant with type int of constant OverridingConstant\Foo::IPSUM.', + 39, + ], + ]); + } + + public function testFinal(): void + { + if (!self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires static reflection.'); + } + + $errors = [ + [ + 'Constant OverridingFinalConstant\Bar::FOO overrides final constant OverridingFinalConstant\Foo::FOO.', + 18, + ], + [ + 'Constant OverridingFinalConstant\Bar::BAR overrides final constant OverridingFinalConstant\Foo::BAR.', + 19, + ], + ]; + + if (PHP_VERSION_ID < 80100) { + $errors[] = [ + 'Constant OverridingFinalConstant\Baz::FOO overrides final constant OverridingFinalConstant\FooInterface::FOO.', + 34, + ]; + } + + $errors[] = [ + 'Constant OverridingFinalConstant\Baz::BAR overrides final constant OverridingFinalConstant\FooInterface::BAR.', + 35, + ]; + + if (PHP_VERSION_ID < 80100) { + $errors[] = [ + 'Constant OverridingFinalConstant\Lorem::FOO overrides final constant OverridingFinalConstant\BarInterface::FOO.', + 51, + ]; + } + + $errors[] = [ + 'Type string of constant OverridingFinalConstant\Lorem::FOO is not covariant with type int of constant OverridingFinalConstant\BarInterface::FOO.', + 51, + ]; + + $errors[] = [ + 'Private constant OverridingFinalConstant\PrivateDolor::PROTECTED_CONST overriding protected constant OverridingFinalConstant\Dolor::PROTECTED_CONST should be protected or public.', + 69, + ]; + $errors[] = [ + 'Private constant OverridingFinalConstant\PrivateDolor::PUBLIC_CONST overriding public constant OverridingFinalConstant\Dolor::PUBLIC_CONST should also be public.', + 70, + ]; + $errors[] = [ + 'Private constant OverridingFinalConstant\PrivateDolor::ANOTHER_PUBLIC_CONST overriding public constant OverridingFinalConstant\Dolor::ANOTHER_PUBLIC_CONST should also be public.', + 71, + ]; + $errors[] = [ + 'Protected constant OverridingFinalConstant\ProtectedDolor::PUBLIC_CONST overriding public constant OverridingFinalConstant\Dolor::PUBLIC_CONST should also be public.', + 80, + ]; + $errors[] = [ + 'Protected constant OverridingFinalConstant\ProtectedDolor::ANOTHER_PUBLIC_CONST overriding public constant OverridingFinalConstant\Dolor::ANOTHER_PUBLIC_CONST should also be public.', + 81, + ]; + + $this->analyse([__DIR__ . '/data/overriding-final-constant.php'], $errors); + } + +} diff --git a/tests/PHPStan/Rules/Constants/data/final-constant.php b/tests/PHPStan/Rules/Constants/data/final-constant.php new file mode 100644 index 0000000000..1c6bc5eab7 --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/final-constant.php @@ -0,0 +1,11 @@ += 8.1 + +namespace FinalConstant; + +class Foo +{ + + const TEST = 1; + final const BAR = 2; + +} diff --git a/tests/PHPStan/Rules/Constants/data/missing-class-constant-typehint.php b/tests/PHPStan/Rules/Constants/data/missing-class-constant-typehint.php new file mode 100644 index 0000000000..6d639da39f --- /dev/null +++ b/tests/PHPStan/Rules/Constants/data/missing-class-constant-typehint.php @@ -0,0 +1,28 @@ + + * @extends RuleTestCase */ -class DateTimeInstantiationRuleTest extends \PHPStan\Testing\RuleTestCase +class DateTimeInstantiationRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new DateTimeInstantiationRule(); } @@ -42,7 +44,7 @@ public function test(): void 'Instantiating DateTime with 2020-04-31 produces a warning: The parsed date was invalid', 20, ],*/ - ] + ], ); } diff --git a/tests/PHPStan/Rules/DeadCode/NoopRuleTest.php b/tests/PHPStan/Rules/DeadCode/NoopRuleTest.php index 23a83440e5..9e58ac9288 100644 --- a/tests/PHPStan/Rules/DeadCode/NoopRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/NoopRuleTest.php @@ -5,9 +5,10 @@ use PhpParser\PrettyPrinter\Standard; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class NoopRuleTest extends RuleTestCase { diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php index 16692000ca..32599a1c70 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -6,13 +6,12 @@ use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class UnreachableStatementRuleTest extends RuleTestCase { - /** @var bool */ - private $treatPhpDocTypesAsCertain; + private bool $treatPhpDocTypesAsCertain; protected function getRule(): Rule { @@ -72,7 +71,6 @@ public function dataBugWithoutGitHubIssue1(): array /** * @dataProvider dataBugWithoutGitHubIssue1 - * @param bool $treatPhpDocTypesAsCertain */ public function testBugWithoutGitHubIssue1(bool $treatPhpDocTypesAsCertain): void { @@ -116,4 +114,10 @@ public function testBug2913(): void $this->analyse([__DIR__ . '/data/bug-2913.php'], []); } + public function testBug4370(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4370.php'], []); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php index a6e12e7cab..4f5d8e8d46 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php @@ -8,6 +8,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use UnusedPrivateConstant\TestExtension; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -28,7 +29,7 @@ public function isAlwaysUsed(ConstantReflection $constant): bool } }, - ]) + ]), ); } @@ -38,12 +39,43 @@ public function testRule(): void [ 'Constant UnusedPrivateConstant\Foo::BAR_CONST is unused.', 10, + 'See: https://phpstan.org/developing-extensions/always-used-class-constants', ], [ 'Constant UnusedPrivateConstant\TestExtension::UNUSED is unused.', 23, + 'See: https://phpstan.org/developing-extensions/always-used-class-constants', ], ]); } + public function testBug5651(): void + { + if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-5651.php'], []); + } + + public function testEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/unused-private-constant-enum.php'], [ + [ + 'Constant UnusedPrivateConstantEnum\Foo::TEST_2 is unused.', + 9, + 'See: https://phpstan.org/developing-extensions/always-used-class-constants', + ], + ]); + } + + public function testBug6758(): void + { + $this->analyse([__DIR__ . '/data/bug-6758.php'], []); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php index b06a4c013d..e821d28851 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -56,4 +57,27 @@ public function testNullsafe(): void $this->analyse([__DIR__ . '/data/nullsafe-unused-private-method.php'], []); } + public function testFirstClassCallable(): void + { + if (PHP_VERSION_ID < 80100 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/callable-unused-private-method.php'], []); + } + + public function testEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/unused-private-method-enum.php'], [ + [ + 'Method UnusedPrivateMethodEnunm\Foo::doBaz() is unused.', + 18, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php index 0df99db626..792e014d60 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php @@ -7,6 +7,7 @@ use PHPStan\Rules\Properties\ReadWritePropertiesExtension; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use function in_array; use const PHP_VERSION_ID; /** @@ -16,10 +17,10 @@ class UnusedPrivatePropertyRuleTest extends RuleTestCase { /** @var string[] */ - private $alwaysWrittenTags; + private array $alwaysWrittenTags; /** @var string[] */ - private $alwaysReadTags; + private array $alwaysReadTags; protected function getRule(): Rule { @@ -54,7 +55,7 @@ public function isInitialized(PropertyReflection $property, string $propertyName ]), $this->alwaysWrittenTags, $this->alwaysReadTags, - true + true, ); } @@ -67,60 +68,95 @@ public function testRule(): void $this->alwaysWrittenTags = []; $this->alwaysReadTags = []; + $tip = 'See: https://phpstan.org/developing-extensions/always-read-written-properties'; + $this->analyse([__DIR__ . '/data/unused-private-property.php'], [ [ 'Property UnusedPrivateProperty\Foo::$bar is never read, only written.', 10, + $tip, ], [ 'Property UnusedPrivateProperty\Foo::$baz is unused.', 12, + $tip, ], [ 'Property UnusedPrivateProperty\Foo::$lorem is never written, only read.', 14, + $tip, ], [ 'Property UnusedPrivateProperty\Bar::$baz is never written, only read.', 57, + $tip, ], [ 'Static property UnusedPrivateProperty\Baz::$bar is never read, only written.', 86, + $tip, ], [ 'Static property UnusedPrivateProperty\Baz::$baz is unused.', 88, + $tip, ], [ 'Static property UnusedPrivateProperty\Baz::$lorem is never written, only read.', 90, + $tip, ], [ 'Property UnusedPrivateProperty\Lorem::$baz is never read, only written.', 117, + $tip, ], [ 'Property class@anonymous/tests/PHPStan/Rules/DeadCode/data/unused-private-property.php:152::$bar is unused.', 153, + $tip, ], [ 'Property UnusedPrivateProperty\DolorWithAnonymous::$foo is unused.', 148, + $tip, + ], + [ + 'Property UnusedPrivateProperty\ArrayAssign::$foo is never read, only written.', + 162, + $tip, + ], + [ + 'Property UnusedPrivateProperty\ListAssign::$foo is never read, only written.', + 191, + $tip, + ], + [ + 'Property UnusedPrivateProperty\WriteToCollection::$collection1 is never read, only written.', + 221, + $tip, + ], + [ + 'Property UnusedPrivateProperty\WriteToCollection::$collection2 is never read, only written.', + 224, + $tip, ], ]); $this->analyse([__DIR__ . '/data/TestExtension.php'], [ [ 'Property UnusedPrivateProperty\TestExtension::$unused is unused.', 8, + $tip, ], [ 'Property UnusedPrivateProperty\TestExtension::$read is never written, only read.', 10, + $tip, ], [ 'Property UnusedPrivateProperty\TestExtension::$written is never read, only written.', 12, + $tip, ], ]); } @@ -129,14 +165,17 @@ public function testAlwaysUsedTags(): void { $this->alwaysWrittenTags = ['@ORM\Column']; $this->alwaysReadTags = ['@get']; + $tip = 'See: https://phpstan.org/developing-extensions/always-read-written-properties'; $this->analyse([__DIR__ . '/data/private-property-with-tags.php'], [ [ 'Property PrivatePropertyWithTags\Foo::$title is never read, only written.', 13, + $tip, ], [ 'Property PrivatePropertyWithTags\Foo::$text is never written, only read.', 18, + $tip, ], ]); } @@ -155,10 +194,12 @@ public function testBug3636(): void } $this->alwaysWrittenTags = []; $this->alwaysReadTags = []; + $tip = 'See: https://phpstan.org/developing-extensions/always-read-written-properties'; $this->analyse([__DIR__ . '/data/bug-3636.php'], [ [ 'Property Bug3636\Bar::$date is never written, only read.', 22, + $tip, ], ]); } @@ -171,10 +212,12 @@ public function testPromotedProperties(): void $this->alwaysWrittenTags = []; $this->alwaysReadTags = ['@get']; + $tip = 'See: https://phpstan.org/developing-extensions/always-read-written-properties'; $this->analyse([__DIR__ . '/data/unused-private-promoted-property.php'], [ [ 'Property UnusedPrivatePromotedProperty\Foo::$lorem is never read, only written.', 12, + $tip, ], ]); } @@ -190,4 +233,33 @@ public function testNullsafe(): void $this->analyse([__DIR__ . '/data/nullsafe-unused-private-property.php'], []); } + public function testBug5935(): void + { + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-5935.php'], []); + } + + public function testBug5337(): void + { + if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/bug-5337.php'], [ + [ + 'Property Bug5337\Clazz::$prefix is never read, only written.', + 7, + 'See: https://phpstan.org/developing-extensions/always-read-written-properties', + ], + [ + 'Property Bug5337\Foo::$field is unused.', + 20, + 'See: https://phpstan.org/developing-extensions/always-read-written-properties', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-4370.php b/tests/PHPStan/Rules/DeadCode/data/bug-4370.php new file mode 100644 index 0000000000..d7c2371985 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-4370.php @@ -0,0 +1,42 @@ +lex(); + + // recursively ignore nested objects + if ($code !== self::JSON_OBJECT_START) { + continue; + } + + $this->ignoreObjectBlock($lexer); + } while ($code !== self::JSON_OBJECT_END); + + return $lexer->lex(); + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-5337.php b/tests/PHPStan/Rules/DeadCode/data/bug-5337.php new file mode 100644 index 0000000000..4051fc7628 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-5337.php @@ -0,0 +1,27 @@ += 7.4 + +namespace Bug5337; + +class Clazz +{ + private ?string $prefix; + + public function setter(string $prefix): void + { + if (!empty($this->prefix)) { + $this->prefix = $prefix; + } + } +} + +class Foo +{ + + private string $field; + + public function __construct() + { + if (isset($this->field)) {} + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-5651.php b/tests/PHPStan/Rules/DeadCode/data/bug-5651.php new file mode 100644 index 0000000000..88068fd018 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-5651.php @@ -0,0 +1,30 @@ +values = $values; + } +} + +class HelloWorld +{ + private const BAR = 'bar'; + + #[MyAttribute(['foo' => self::BAR])] + public function sayHello(): void + { + + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-5935.php b/tests/PHPStan/Rules/DeadCode/data/bug-5935.php new file mode 100644 index 0000000000..7a842a8fb7 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-5935.php @@ -0,0 +1,45 @@ +arr; + if(isset($arr[$id])) + { + return $arr[$id]; + } + else + { + return $arr[$id] = time(); + } + } +} + +class Test2 +{ + /** + * @var int[] + */ + private $arr; + + public function test(int $id): int + { + $arr = &$this->arr; + if(isset($arr[$id])) + { + return $arr[$id]; + } + else + { + return $arr[$id] = time(); + } + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-6758.php b/tests/PHPStan/Rules/DeadCode/data/bug-6758.php new file mode 100644 index 0000000000..690baf82ca --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-6758.php @@ -0,0 +1,27 @@ += 8.1 + +namespace CallableUnusedPrivateMethod; + +class Foo +{ + + public function doFoo(): void + { + $f = $this->doBar(...); + } + + private function doBar(): void + { + + } + +} + +class Bar +{ + + public function doFoo(): void + { + $f = self::doBar(...); + } + + private static function doBar(): void + { + + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-private-constant-enum.php b/tests/PHPStan/Rules/DeadCode/data/unused-private-constant-enum.php new file mode 100644 index 0000000000..63daa4fb44 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-private-constant-enum.php @@ -0,0 +1,16 @@ += 8.1 + +namespace UnusedPrivateConstantEnum; + +enum Foo +{ + + private const TEST = 1; + private const TEST_2 = 1; + + public function doFoo(): void + { + echo self::TEST; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-private-constant.php b/tests/PHPStan/Rules/DeadCode/data/unused-private-constant.php index ca5e53fba9..88531598b2 100644 --- a/tests/PHPStan/Rules/DeadCode/data/unused-private-constant.php +++ b/tests/PHPStan/Rules/DeadCode/data/unused-private-constant.php @@ -22,3 +22,25 @@ class TestExtension private const UNUSED = 2; } + + +final class P +{ + + private const JSON_OBJECT_START = 17; + private const JSON_OBJECT_END = 18; + + public function ignoreObjectBlock(): void + { + do { + $code = doFoo(); + + // recursively ignore nested objects + if ($code !== self::JSON_OBJECT_START) { + continue; + } + + $this->ignoreObjectBlock(); + } while ($code !== self::JSON_OBJECT_END); + } +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-private-method-enum.php b/tests/PHPStan/Rules/DeadCode/data/unused-private-method-enum.php new file mode 100644 index 0000000000..d3ab7e71eb --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-private-method-enum.php @@ -0,0 +1,23 @@ += 8.1 + +namespace UnusedPrivateMethodEnunm; + +enum Foo +{ + + public function doFoo(): void + { + $this->doBar(); + } + + private function doBar(): void + { + + } + + private function doBaz(): void + { + + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-private-property.php b/tests/PHPStan/Rules/DeadCode/data/unused-private-property.php index 3511bee8a9..e5b8841eb9 100644 --- a/tests/PHPStan/Rules/DeadCode/data/unused-private-property.php +++ b/tests/PHPStan/Rules/DeadCode/data/unused-private-property.php @@ -155,3 +155,78 @@ public function doFoo() } } + +class ArrayAssign +{ + + private $foo; + + public function doFoo(): void + { + [$this->foo] = [1]; + } + +} + +class ArrayAssignAndRead +{ + + private $foo; + + public function doFoo(): void + { + [$this->foo] = [1]; + } + + public function getFoo() + { + return $this->foo; + } + +} + +class ListAssign +{ + + private $foo; + + public function doFoo(): void + { + list($this->foo) = [1]; + } + +} + +class ListAssignAndRead +{ + + private $foo; + + public function doFoo(): void + { + list($this->foo) = [1]; + } + + public function getFoo() + { + return $this->foo; + } + +} + +class WriteToCollection +{ + + /** @var \ArrayAccess */ + private $collection1; + + /** @var \ArrayAccess&\Countable */ + private $collection2; + + public function foo(): void + { + $this->collection1[] = 1; + $this->collection2[] = 2; + } + +} diff --git a/tests/PHPStan/Rules/Debug/DumpTypeRuleTest.php b/tests/PHPStan/Rules/Debug/DumpTypeRuleTest.php index f172d91e98..d611afb9ef 100644 --- a/tests/PHPStan/Rules/Debug/DumpTypeRuleTest.php +++ b/tests/PHPStan/Rules/Debug/DumpTypeRuleTest.php @@ -20,7 +20,7 @@ public function testRuleInPhpStanNamespace(): void { $this->analyse([__DIR__ . '/data/dump-type.php'], [ [ - 'Dumped type: array&nonEmpty', + 'Dumped type: non-empty-array', 10, ], [ @@ -34,7 +34,7 @@ public function testRuleInDifferentNamespace(): void { $this->analyse([__DIR__ . '/data/dump-type-ns.php'], [ [ - 'Dumped type: array&nonEmpty', + 'Dumped type: non-empty-array', 10, ], ]); @@ -44,11 +44,11 @@ public function testRuleInUse(): void { $this->analyse([__DIR__ . '/data/dump-type-use.php'], [ [ - 'Dumped type: array&nonEmpty', + 'Dumped type: non-empty-array', 12, ], [ - 'Dumped type: array&nonEmpty', + 'Dumped type: non-empty-array', 13, ], ]); diff --git a/tests/PHPStan/Rules/Debug/data/file-asserts.php b/tests/PHPStan/Rules/Debug/data/file-asserts.php index 5f9fb9cc8d..abd8e54d07 100644 --- a/tests/PHPStan/Rules/Debug/data/file-asserts.php +++ b/tests/PHPStan/Rules/Debug/data/file-asserts.php @@ -24,7 +24,7 @@ public function doFoo(array $a): void */ public function doBar(array $a): void { - assertType('array&nonEmpty', $a); + assertType('non-empty-array', $a); assertNativeType('array', $a); assertType('false', $a === []); diff --git a/tests/PHPStan/Rules/DummyRule.php b/tests/PHPStan/Rules/DummyRule.php index 697c9a4ead..02f6939690 100644 --- a/tests/PHPStan/Rules/DummyRule.php +++ b/tests/PHPStan/Rules/DummyRule.php @@ -6,9 +6,9 @@ use PHPStan\Analyser\Scope; /** - * @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\FuncCall> + * @implements Rule */ -class DummyRule implements \PHPStan\Rules\Rule +class DummyRule implements Rule { public function getNodeType(): string diff --git a/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php b/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php new file mode 100644 index 0000000000..1669ec0c2a --- /dev/null +++ b/tests/PHPStan/Rules/EnumCases/EnumCaseAttributesRuleTest.php @@ -0,0 +1,59 @@ + + */ +class EnumCaseAttributesRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + return new EnumCaseAttributesRule( + new AttributesCheck( + $reflectionProvider, + new FunctionCallParametersCheck( + new RuleLevelHelper($reflectionProvider, true, false, true, false), + new NullsafeCheck(), + new PhpVersion(80100), + new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), + true, + true, + true, + true, + ), + new ClassCaseSensitivityCheck($reflectionProvider, false), + ), + ); + } + + public function testRule(): void + { + if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/enum-case-attributes.php'], [ + [ + 'Attribute class EnumCaseAttributes\AttributeWithPropertyTarget does not have the class constant target.', + 26, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/EnumCases/data/enum-case-attributes.php b/tests/PHPStan/Rules/EnumCases/data/enum-case-attributes.php new file mode 100644 index 0000000000..74c4f6c4ff --- /dev/null +++ b/tests/PHPStan/Rules/EnumCases/data/enum-case-attributes.php @@ -0,0 +1,45 @@ += 8.1 + +namespace EnumCaseAttributes; + +#[\Attribute(\Attribute::TARGET_PROPERTY)] +class AttributeWithPropertyTarget +{ + +} + +#[\Attribute(\Attribute::TARGET_CLASS_CONSTANT)] +class AttributeWithClassConstantTarget +{ + +} + +#[\Attribute(\Attribute::TARGET_ALL)] +class AttributeWithTargetAll +{ + +} + +enum Lorem +{ + + #[AttributeWithPropertyTarget] + case FOO; + +} + +enum Ipsum +{ + + #[AttributeWithClassConstantTarget] + case FOO; + +} + +enum Dolor +{ + + #[AttributeWithTargetAll] + case FOO; + +} diff --git a/tests/PHPStan/Rules/Exceptions/Bug5364Test.php b/tests/PHPStan/Rules/Exceptions/Bug5364Test.php new file mode 100644 index 0000000000..e6c4a38a42 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/Bug5364Test.php @@ -0,0 +1,39 @@ + + */ +class Bug5364Test extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new MissingCheckedExceptionInMethodThrowsRule( + new MissingCheckedExceptionInThrowsCheck(new DefaultExceptionTypeResolver( + $this->createReflectionProvider(), + [], + [], + [], + [], + )), + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/bug-5364.php'], []); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/bug-5364.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php index 51dbd339d6..ea8625fb5f 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -103,6 +104,10 @@ public function testRule(): void 'Dead catch - Exception is never thrown in the try block.', 485, ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 532, + ], ]); } @@ -167,4 +172,148 @@ public function testThrowExpression(): void ]); } + public function testDeadCatch(): void + { + $this->analyse([__DIR__ . '/data/dead-catch.php'], [ + [ + 'Dead catch - TypeError is already caught above.', + 27, + ], + ]); + } + + public function testFirstClassCallables(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/dead-catch-first-class-callables.php'], [ + [ + 'Dead catch - InvalidArgumentException is never thrown in the try block.', + 29, + ], + ]); + } + + public function testBug4852(): void + { + if (!self::$useStaticReflectionProvider) { + $this->markTestSkipped('This test needs static reflection'); + } + + $this->analyse([__DIR__ . '/data/bug-4852.php'], [ + [ + 'Dead catch - Exception is never thrown in the try block.', + 70, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 77, + ], + ]); + } + + public function testBug5903(): void + { + $this->analyse([__DIR__ . '/data/bug-5903.php'], [ + [ + 'Dead catch - Throwable is never thrown in the try block.', + 47, + ], + [ + 'Dead catch - Throwable is never thrown in the try block.', + 54, + ], + ]); + } + + public function testBug6262(): void + { + $this->analyse([__DIR__ . '/data/bug-6262.php'], []); + } + + public function testBug6256(): void + { + if (PHP_VERSION_ID < 70400) { + self::markTestSkipped('Test requires PHP 7.4.'); + } + + $this->analyse([__DIR__ . '/data/bug-6256.php'], [ + [ + 'Dead catch - TypeError is never thrown in the try block.', + 25, + ], + [ + 'Dead catch - TypeError is never thrown in the try block.', + 31, + ], + [ + 'Dead catch - TypeError is never thrown in the try block.', + 45, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 57, + ], + [ + 'Dead catch - Throwable is never thrown in the try block.', + 63, + ], + [ + 'Dead catch - Exception is never thrown in the try block.', + 100, + ], + ]); + } + + public function testBug6791(): void + { + if (PHP_VERSION_ID < 70400) { + self::markTestSkipped('Test requires PHP 7.4.'); + } + + $this->analyse([__DIR__ . '/data/bug-6791.php'], [ + [ + 'Dead catch - TypeError is never thrown in the try block.', + 22, + ], + [ + 'Dead catch - TypeError is never thrown in the try block.', + 34, + ], + [ + 'Dead catch - TypeError is never thrown in the try block.', + 38, + ], + ]); + } + + public function testBug6786(): void + { + if (PHP_VERSION_ID < 70400) { + self::markTestSkipped('Test requires PHP 7.4.'); + } + + $this->analyse([__DIR__ . '/data/bug-6786.php'], []); + } + + public function testUnionTypeError(): void + { + if (PHP_VERSION_ID < 80000) { + self::markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/union-type-error.php'], [ + [ + 'Dead catch - TypeError is never thrown in the try block.', + 14, + ], + [ + 'Dead catch - TypeError is never thrown in the try block.', + 22, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Exceptions/CaughtExceptionExistenceRuleTest.php b/tests/PHPStan/Rules/Exceptions/CaughtExceptionExistenceRuleTest.php index 9875365e55..6af5ae9a2c 100644 --- a/tests/PHPStan/Rules/Exceptions/CaughtExceptionExistenceRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CaughtExceptionExistenceRuleTest.php @@ -3,20 +3,22 @@ namespace PHPStan\Rules\Exceptions; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class CaughtExceptionExistenceRuleTest extends \PHPStan\Testing\RuleTestCase +class CaughtExceptionExistenceRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { $broker = $this->createReflectionProvider(); return new CaughtExceptionExistenceRule( $broker, - new ClassCaseSensitivityCheck($broker), - true + new ClassCaseSensitivityCheck($broker, true), + true, ); } diff --git a/tests/PHPStan/Rules/Exceptions/DeadCatchRuleTest.php b/tests/PHPStan/Rules/Exceptions/DeadCatchRuleTest.php deleted file mode 100644 index 828cab35ce..0000000000 --- a/tests/PHPStan/Rules/Exceptions/DeadCatchRuleTest.php +++ /dev/null @@ -1,29 +0,0 @@ - - */ -class DeadCatchRuleTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new DeadCatchRule(); - } - - public function testRule(): void - { - $this->analyse([__DIR__ . '/data/dead-catch.php'], [ - [ - 'Dead catch - TypeError is already caught by Throwable above.', - 27, - ], - ]); - } - -} diff --git a/tests/PHPStan/Rules/Exceptions/DefaultExceptionTypeResolverTest.php b/tests/PHPStan/Rules/Exceptions/DefaultExceptionTypeResolverTest.php index bfcaf40aea..0708028924 100644 --- a/tests/PHPStan/Rules/Exceptions/DefaultExceptionTypeResolverTest.php +++ b/tests/PHPStan/Rules/Exceptions/DefaultExceptionTypeResolverTest.php @@ -2,11 +2,14 @@ namespace PHPStan\Rules\Exceptions; +use DomainException; +use InvalidArgumentException; +use LogicException; use PHPStan\Analyser\ScopeContext; use PHPStan\Analyser\ScopeFactory; -use PHPStan\Testing\TestCase; +use PHPStan\Testing\PHPStanTestCase; -class DefaultExceptionTypeResolverTest extends TestCase +class DefaultExceptionTypeResolverTest extends PHPStanTestCase { public function dataIsCheckedException(): array @@ -17,7 +20,7 @@ public function dataIsCheckedException(): array [], [], [], - \InvalidArgumentException::class, + InvalidArgumentException::class, true, ], [ @@ -27,47 +30,47 @@ public function dataIsCheckedException(): array [], [], [], - \InvalidArgumentException::class, + InvalidArgumentException::class, false, ], [ [], [ - \InvalidArgumentException::class, + InvalidArgumentException::class, ], [], [], - \InvalidArgumentException::class, + InvalidArgumentException::class, false, ], [ [], [ - \LogicException::class, + LogicException::class, ], [], [], - \LogicException::class, + LogicException::class, false, ], [ [], [ - \LogicException::class, + LogicException::class, ], [], [], - \DomainException::class, + DomainException::class, false, ], [ [], [ - \DomainException::class, + DomainException::class, ], [], [], - \LogicException::class, + LogicException::class, true, ], [ @@ -77,7 +80,7 @@ public function dataIsCheckedException(): array '#^Exception$#', ], [], - \InvalidArgumentException::class, + InvalidArgumentException::class, false, ], [ @@ -87,7 +90,7 @@ public function dataIsCheckedException(): array '#^InvalidArgumentException#', ], [], - \InvalidArgumentException::class, + InvalidArgumentException::class, true, ], [ @@ -95,9 +98,9 @@ public function dataIsCheckedException(): array [], [], [ - \DomainException::class, + DomainException::class, ], - \InvalidArgumentException::class, + InvalidArgumentException::class, false, ], [ @@ -105,9 +108,9 @@ public function dataIsCheckedException(): array [], [], [ - \InvalidArgumentException::class, + InvalidArgumentException::class, ], - \InvalidArgumentException::class, + InvalidArgumentException::class, true, ], [ @@ -115,9 +118,9 @@ public function dataIsCheckedException(): array [], [], [ - \LogicException::class, + LogicException::class, ], - \InvalidArgumentException::class, + InvalidArgumentException::class, true, ], ]; @@ -129,8 +132,6 @@ public function dataIsCheckedException(): array * @param string[] $uncheckedExceptionClasses * @param string[] $checkedExceptionRegexes * @param string[] $checkedExceptionClasses - * @param string $className - * @param bool $expectedResult */ public function testIsCheckedException( array $uncheckedExceptionRegexes, @@ -138,10 +139,10 @@ public function testIsCheckedException( array $checkedExceptionRegexes, array $checkedExceptionClasses, string $className, - bool $expectedResult + bool $expectedResult, ): void { - $resolver = new DefaultExceptionTypeResolver($this->createBroker(), $uncheckedExceptionRegexes, $uncheckedExceptionClasses, $checkedExceptionRegexes, $checkedExceptionClasses); + $resolver = new DefaultExceptionTypeResolver($this->createReflectionProvider(), $uncheckedExceptionRegexes, $uncheckedExceptionClasses, $checkedExceptionRegexes, $checkedExceptionClasses); $this->assertSame($expectedResult, $resolver->isCheckedException($className, self::getContainer()->getByType(ScopeFactory::class)->create(ScopeContext::create(__DIR__)))); } diff --git a/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRuleTest.php b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRuleTest.php index 67b7f7c96a..716e7b8687 100644 --- a/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInFunctionThrowsRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Exceptions; use PHPStan\Rules\Rule; +use PHPStan\ShouldNotHappenException; use PHPStan\Testing\RuleTestCase; /** @@ -17,10 +18,10 @@ protected function getRule(): Rule new MissingCheckedExceptionInThrowsCheck(new DefaultExceptionTypeResolver( $this->createReflectionProvider(), [], - [\PHPStan\ShouldNotHappenException::class], + [ShouldNotHappenException::class], [], - [] - )) + [], + )), ); } diff --git a/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php index 97797eef37..9f195bb503 100644 --- a/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Exceptions; use PHPStan\Rules\Rule; +use PHPStan\ShouldNotHappenException; use PHPStan\Testing\RuleTestCase; /** @@ -17,10 +18,10 @@ protected function getRule(): Rule new MissingCheckedExceptionInThrowsCheck(new DefaultExceptionTypeResolver( $this->createReflectionProvider(), [], - [\PHPStan\ShouldNotHappenException::class], + [ShouldNotHappenException::class], [], - [] - )) + [], + )), ); } diff --git a/tests/PHPStan/Rules/Exceptions/OverwrittenExitPointByFinallyRuleTest.php b/tests/PHPStan/Rules/Exceptions/OverwrittenExitPointByFinallyRuleTest.php index 268b220f34..1698ada07c 100644 --- a/tests/PHPStan/Rules/Exceptions/OverwrittenExitPointByFinallyRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/OverwrittenExitPointByFinallyRuleTest.php @@ -19,6 +19,10 @@ protected function getRule(): Rule public function testRule(): void { $this->analyse([__DIR__ . '/data/overwritten-exit-point.php'], [ + [ + 'This throw is overwritten by a different one in the finally block below.', + 8, + ], [ 'This return is overwritten by a different one in the finally block below.', 11, @@ -34,4 +38,142 @@ public function testRule(): void ]); } + public function testBug5627(): void + { + $this->analyse([__DIR__ . '/data/bug-5627.php'], [ + [ + 'This throw is overwritten by a different one in the finally block below.', + 10, + ], + [ + 'This throw is overwritten by a different one in the finally block below.', + 12, + ], + [ + 'The overwriting return is on this line.', + 14, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 29, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 31, + ], + [ + 'The overwriting return is on this line.', + 33, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 39, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 41, + ], + [ + 'The overwriting return is on this line.', + 43, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 49, + ], + [ + 'The overwriting return is on this line.', + 51, + ], + [ + 'This throw is overwritten by a different one in the finally block below.', + 62, + ], + [ + 'This throw is overwritten by a different one in the finally block below.', + 64, + ], + [ + 'The overwriting return is on this line.', + 66, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 81, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 83, + ], + [ + 'The overwriting return is on this line.', + 85, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 91, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 93, + ], + [ + 'The overwriting return is on this line.', + 95, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 101, + ], + [ + 'The overwriting return is on this line.', + 103, + ], + [ + 'This throw is overwritten by a different one in the finally block below.', + 122, + ], + [ + 'This throw is overwritten by a different one in the finally block below.', + 124, + ], + [ + 'The overwriting return is on this line.', + 126, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 141, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 143, + ], + [ + 'The overwriting return is on this line.', + 145, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 151, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 153, + ], + [ + 'The overwriting return is on this line.', + 155, + ], + [ + 'This exit point is overwritten by a different one in the finally block below.', + 161, + ], + [ + 'The overwriting return is on this line.', + 163, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Exceptions/ThrowExpressionRuleTest.php b/tests/PHPStan/Rules/Exceptions/ThrowExpressionRuleTest.php index 2134ca920c..ced6add41a 100644 --- a/tests/PHPStan/Rules/Exceptions/ThrowExpressionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/ThrowExpressionRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -12,8 +13,7 @@ class ThrowExpressionRuleTest extends RuleTestCase { - /** @var PhpVersion */ - private $phpVersion; + private PhpVersion $phpVersion; protected function getRule(): Rule { @@ -41,7 +41,6 @@ public function dataRule(): array /** * @dataProvider dataRule - * @param int $phpVersion * @param mixed[] $expectedErrors */ public function testRule(int $phpVersion, array $expectedErrors): void diff --git a/tests/PHPStan/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRuleTest.php b/tests/PHPStan/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRuleTest.php new file mode 100644 index 0000000000..5b78999ddd --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/ThrowsVoidFunctionWithExplicitThrowPointRuleTest.php @@ -0,0 +1,99 @@ + + */ +class ThrowsVoidFunctionWithExplicitThrowPointRuleTest extends RuleTestCase +{ + + private bool $missingCheckedExceptionInThrows; + + /** @var string[] */ + private array $checkedExceptionClasses; + + protected function getRule(): Rule + { + return new ThrowsVoidFunctionWithExplicitThrowPointRule(new DefaultExceptionTypeResolver( + $this->createReflectionProvider(), + [], + [], + [], + $this->checkedExceptionClasses, + ), $this->missingCheckedExceptionInThrows); + } + + public function dataRule(): array + { + return [ + [ + true, + [], + [], + ], + [ + false, + ['DifferentException'], + [ + [ + 'Function ThrowsVoidFunction\foo() throws exception ThrowsVoidFunction\MyException but the PHPDoc contains @throws void.', + 15, + ], + ], + ], + [ + true, + [MyException::class], + [], + ], + [ + true, + ['DifferentException'], + [ + [ + 'Function ThrowsVoidFunction\foo() throws exception ThrowsVoidFunction\MyException but the PHPDoc contains @throws void.', + 15, + ], + ], + ], + [ + false, + [], + [ + [ + 'Function ThrowsVoidFunction\foo() throws exception ThrowsVoidFunction\MyException but the PHPDoc contains @throws void.', + 15, + ], + ], + ], + [ + false, + [MyException::class], + [ + [ + 'Function ThrowsVoidFunction\foo() throws exception ThrowsVoidFunction\MyException but the PHPDoc contains @throws void.', + 15, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataRule + * @param string[] $checkedExceptionClasses + * @param mixed[] $errors + */ + public function testRule(bool $missingCheckedExceptionInThrows, array $checkedExceptionClasses, array $errors): void + { + $this->missingCheckedExceptionInThrows = $missingCheckedExceptionInThrows; + $this->checkedExceptionClasses = $checkedExceptionClasses; + $this->analyse([__DIR__ . '/data/throws-void-function.php'], $errors); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php b/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php new file mode 100644 index 0000000000..feecdd2ac1 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/ThrowsVoidMethodWithExplicitThrowPointRuleTest.php @@ -0,0 +1,99 @@ + + */ +class ThrowsVoidMethodWithExplicitThrowPointRuleTest extends RuleTestCase +{ + + private bool $missingCheckedExceptionInThrows; + + /** @var string[] */ + private array $checkedExceptionClasses; + + protected function getRule(): Rule + { + return new ThrowsVoidMethodWithExplicitThrowPointRule(new DefaultExceptionTypeResolver( + $this->createReflectionProvider(), + [], + [], + [], + $this->checkedExceptionClasses, + ), $this->missingCheckedExceptionInThrows); + } + + public function dataRule(): array + { + return [ + [ + true, + [], + [], + ], + [ + false, + ['DifferentException'], + [ + [ + 'Method ThrowsVoidMethod\Foo::doFoo() throws exception ThrowsVoidMethod\MyException but the PHPDoc contains @throws void.', + 18, + ], + ], + ], + [ + true, + [MyException::class], + [], + ], + [ + true, + ['DifferentException'], + [ + [ + 'Method ThrowsVoidMethod\Foo::doFoo() throws exception ThrowsVoidMethod\MyException but the PHPDoc contains @throws void.', + 18, + ], + ], + ], + [ + false, + [], + [ + [ + 'Method ThrowsVoidMethod\Foo::doFoo() throws exception ThrowsVoidMethod\MyException but the PHPDoc contains @throws void.', + 18, + ], + ], + ], + [ + false, + [MyException::class], + [ + [ + 'Method ThrowsVoidMethod\Foo::doFoo() throws exception ThrowsVoidMethod\MyException but the PHPDoc contains @throws void.', + 18, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataRule + * @param string[] $checkedExceptionClasses + * @param mixed[] $errors + */ + public function testRule(bool $missingCheckedExceptionInThrows, array $checkedExceptionClasses, array $errors): void + { + $this->missingCheckedExceptionInThrows = $missingCheckedExceptionInThrows; + $this->checkedExceptionClasses = $checkedExceptionClasses; + $this->analyse([__DIR__ . '/data/throws-void-method.php'], $errors); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/bug-5364.neon b/tests/PHPStan/Rules/Exceptions/bug-5364.neon new file mode 100644 index 0000000000..93a3c1512a --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/bug-5364.neon @@ -0,0 +1,6 @@ +parameters: + exceptions: + implicitThrows: false + check: + missingCheckedExceptionInThrows: true + tooWideThrowType: true diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-4852.php b/tests/PHPStan/Rules/Exceptions/data/bug-4852.php new file mode 100644 index 0000000000..058f6c1a1f --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-4852.php @@ -0,0 +1,80 @@ +test(); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-5627.php b/tests/PHPStan/Rules/Exceptions/data/bug-5627.php new file mode 100644 index 0000000000..570d8aabdc --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-5627.php @@ -0,0 +1,167 @@ +abort(); + } catch (\Exception $e) { + $this->abort(); + } finally { + return 'finally'; + } + } + + public function c(): string { + try { + $this->abort(); + } catch (\Throwable $e) { + $this->abort(); + } finally { + return 'finally'; + } + } + + public function d(): string { + try { + $this->abort(); + } finally { + return 'finally'; + } + } + +} + +class Bar +{ + + public function a(): string { + try { + throw new \Exception('try'); + } catch (\Exception $e) { + throw new \Exception('catch'); + } finally { + return 'finally'; + } + } + + /** + * + * @return never + */ + public function abort() + { + throw new \Exception(); + } + + public function b(): string { + try { + $this->abort(); + } catch (\Exception $e) { + $this->abort(); + } finally { + return 'finally'; + } + } + + public function c(): string { + try { + $this->abort(); + } catch (\Throwable $e) { + $this->abort(); + } finally { + return 'finally'; + } + } + + public function d(): string { + try { + $this->abort(); + } finally { + return 'finally'; + } + } + +} + +/** + * @return never + */ +function abort() +{ + +} + +class Baz +{ + + public function a(): string { + try { + throw new \Exception('try'); + } catch (\Exception $e) { + throw new \Exception('catch'); + } finally { + return 'finally'; + } + } + + + + + + + + + + + public function b(): string { + try { + abort(); + } catch (\Exception $e) { + abort(); + } finally { + return 'finally'; + } + } + + public function c(): string { + try { + abort(); + } catch (\Throwable $e) { + abort(); + } finally { + return 'finally'; + } + } + + public function d(): string { + try { + abort(); + } finally { + return 'finally'; + } + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-5903.php b/tests/PHPStan/Rules/Exceptions/data/bug-5903.php new file mode 100644 index 0000000000..b4c12e3877 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-5903.php @@ -0,0 +1,64 @@ + */ + protected $traversable; + /** @var \Iterator */ + protected $iterator; + /** @var iterable */ + protected $iterable; + /** @var array */ + protected $array; + /** @var array|null */ + protected $maybeArray; + /** @var \Iterator|null */ + protected $maybeIterable; + + public function foo() + { + try { + foreach ($this->traversable as $val) { + echo $val; + } + } catch (\Throwable $e) { + } + + try { + foreach ($this->iterator as $val) { + echo $val; + } + } catch (\Throwable $e) { + } + + try { + foreach ($this->iterable as $val) { + echo $val; + } + } catch (\Throwable $e) { + } + + try { + foreach ($this->array as $val) { + echo $val; + } + } catch (\Throwable $e) { + } + + try { + foreach ($this->maybeArray as $val) { + echo $val; + } + } catch (\Throwable $e) { + } + + try { + foreach ($this->maybeIterable as $val) { + echo $val; + } + } catch (\Throwable $e) { + } + } +} diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-6256.php b/tests/PHPStan/Rules/Exceptions/data/bug-6256.php new file mode 100644 index 0000000000..c7cb4766ad --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-6256.php @@ -0,0 +1,115 @@ += 7.4 + +namespace Bug6256; + +use Exception; + +final class A +{ + public int $integerType = 1; + public $mixedType; + public string $stringType; + /** @var string|int */ + public $stringOrIntType; + + function doFoo() + { + try { + $this->integerType = "string"; + } catch (\TypeError $e) { + // not dead + } + + try { + $this->mixedType = "string"; + } catch (\TypeError $e) { + // dead + } + + try { + $this->stringType = "string"; + } catch (\TypeError $e) { + // dead + } + + /** @var string|int $intOrString */ + $intOrString = ''; + try { + $this->integerType = $intOrString; + } catch (\TypeError $e) { + // not dead + } + + try { + $this->stringOrIntType = 1; + } catch (\TypeError $e) { + // dead + } + + try { + $this->integerType = "string"; + } catch (\Error $e) { + // not dead + } + + try { + $this->integerType = "string"; + } catch (\Exception $e) { + // dead + } + + try { + $this->dynamicProperty = 1; + } catch (\Throwable $e) { + // dead + } + } +} + +final class B { + + /** + * @throws Exception + */ + public function __set(string $name, $value) + { + throw new Exception(); + } + + function doFoo() + { + try { + $this->dynamicProperty = "string"; + } catch (\Exception $e) { + // not dead + } + } +} + +final class C { + + /** + * @throws void + */ + public function __set(string $name, $value) {} + + function doFoo() + { + try { + $this->dynamicProperty = "string"; + } catch (\Exception $e) { + // dead + } + } +} + +class D { + function doFoo() + { + try { + $this->dynamicProperty = "string"; + } catch (\Exception $e) { + // not dead because class is not final + } + } +} diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-6262.php b/tests/PHPStan/Rules/Exceptions/data/bug-6262.php new file mode 100644 index 0000000000..d11b718d17 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-6262.php @@ -0,0 +1,20 @@ += 7.4 + +declare(strict_types = 1); + +namespace Bug6786; + +class HelloWorld +{ + protected int $id; + protected string $code; + protected bool $suggest; + + /** + * @param array|mixed[] $row + * @return void + */ + protected function mapping(array $row): void + { + $this->id = (int) $row['id']; + $this->code = $row['code']; + $this->suggest = (bool) $row['suggest']; + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-6791.php b/tests/PHPStan/Rules/Exceptions/data/bug-6791.php new file mode 100644 index 0000000000..e02f88031a --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-6791.php @@ -0,0 +1,43 @@ += 7.4 + +namespace Bug6791; + +class Foo { + /** @var int[] */ + public array $intArray; + /** @var \Ds\Set */ + public \Ds\Set $set; + /** @var int[] */ + public $array; +} + +class Bar +{ + + public function doFoo() + { + $foo = new Foo(); + try { + $foo->intArray = ["a"]; + } catch (\TypeError $e) {} + + try { + $foo->set = ["a"]; + } catch (\TypeError $e) {} + + try { + $foo->set = new \Ds\Set; + } catch (\TypeError $e) {} + + try { + $foo->array = ["a"]; + } catch (\TypeError $e) {} + + try { + $foo->array = "non-array"; + } catch (\TypeError $e) {} + } + +} + + diff --git a/tests/PHPStan/Rules/Exceptions/data/dead-catch-first-class-callables.php b/tests/PHPStan/Rules/Exceptions/data/dead-catch-first-class-callables.php new file mode 100644 index 0000000000..a212159e99 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/dead-catch-first-class-callables.php @@ -0,0 +1,34 @@ += 8.1 + +namespace DeadCatchFirstClassCallables; + +class Foo +{ + + public function doFoo(): void + { + try { + $this->doBar(); + } catch (\InvalidArgumentException $e) { + + } + } + + /** + * @throws \InvalidArgumentException + */ + public function doBar(): void + { + throw new \InvalidArgumentException(); + } + + public function doBaz(): void + { + try { + $this->doBar(...); + } catch (\InvalidArgumentException $e) { + + } + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/data/dead-catch.php b/tests/PHPStan/Rules/Exceptions/data/dead-catch.php index a2765c41d9..cb583d37c3 100644 --- a/tests/PHPStan/Rules/Exceptions/data/dead-catch.php +++ b/tests/PHPStan/Rules/Exceptions/data/dead-catch.php @@ -8,7 +8,7 @@ class Foo public function doFoo() { try { - + doFoo(); } catch (\Exception $e) { } catch (\TypeError $e) { @@ -21,7 +21,7 @@ public function doFoo() public function doBar() { try { - + doFoo(); } catch (\Throwable $e) { } catch (\TypeError $e) { @@ -29,4 +29,11 @@ public function doBar() } } + function evalString(string $evalString) { + try { + eval($evalString); + } catch (\Throwable $exception) { + // + } + } } diff --git a/tests/PHPStan/Rules/Exceptions/data/throws-void-function.php b/tests/PHPStan/Rules/Exceptions/data/throws-void-function.php new file mode 100644 index 0000000000..d5a407ec13 --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/throws-void-function.php @@ -0,0 +1,16 @@ += 8.0 + +declare(strict_types = 1); + +namespace UnionTypeError; + +class Foo { + public string|int $stringOrInt; + public string|array $stringOrArray; + + public function bar() { + try { + $this->stringOrInt = ""; + } catch (\TypeError $e) {} + + try { + $this->stringOrInt = true; + } catch (\TypeError $e) {} + + try { + $this->stringOrArray = []; + } catch (\TypeError $e) {} + + try { + $this->stringOrInt = $this->stringOrArray; + } catch (\TypeError $e) {} + } +} + diff --git a/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php index 39bdaee5cf..78236201ac 100644 --- a/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php +++ b/tests/PHPStan/Rules/Exceptions/data/unthrown-exception.php @@ -521,3 +521,26 @@ public function doBaz(string $string): void } } + +class ExceptionGetMessage +{ + + public function doFoo(\Exception $e) + { + try { + echo $e->getMessage(); + } catch (\Exception $t) { + + } + } + + public function doBar(string $s) + { + try { + $this->{'doFoo' . $s}(); + } catch (\InvalidArgumentException $e) { + + } + } + +} diff --git a/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php index 63c9647b55..8d91e66080 100644 --- a/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrowFunctionAttributesRuleTest.php @@ -8,9 +8,11 @@ use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -25,18 +27,18 @@ protected function getRule(): Rule new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true), + new RuleLevelHelper($reflectionProvider, true, false, true, false), new NullsafeCheck(), new PhpVersion(80000), - new UnresolvableTypeHelper(true), + new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), true, true, true, true, - true ), - new ClassCaseSensitivityCheck($reflectionProvider, false) - ) + new ClassCaseSensitivityCheck($reflectionProvider, false), + ), ); } diff --git a/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php index a3a584919b..b82fea8ce4 100644 --- a/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrowFunctionReturnTypeRuleTest.php @@ -6,9 +6,10 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class ArrowFunctionReturnTypeRuleTest extends RuleTestCase { @@ -20,7 +21,7 @@ protected function getRule(): Rule true, false, true, - false + false, ))); } diff --git a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php index 7535f3b8c0..1ef3486ea4 100644 --- a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php @@ -6,31 +6,37 @@ use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class CallCallablesRuleTest extends \PHPStan\Testing\RuleTestCase +class CallCallablesRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + private bool $checkExplicitMixed = false; + + protected function getRule(): Rule { - $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false); + $ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed); return new CallCallablesRule( new FunctionCallParametersCheck( $ruleLevelHelper, new NullsafeCheck(), new PhpVersion(80000), - new UnresolvableTypeHelper(true), + new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), true, true, true, true, - true ), $ruleLevelHelper, - true + true, ); } @@ -53,11 +59,11 @@ public function testRule(): void 25, ], [ - 'Parameter #1 $i of callable array($this(CallCallables\Foo), \'doBar\') expects int, string given.', + 'Parameter #1 $i of callable array{$this(CallCallables\\Foo), \'doBar\'} expects int, string given.', 33, ], [ - 'Callable array(\'CallCallables\\\\Foo\', \'doStaticBaz\') invoked with 1 parameter, 0 required.', + 'Callable array{\'CallCallables\\\\Foo\', \'doStaticBaz\'} invoked with 1 parameter, 0 required.', 39, ], [ @@ -106,7 +112,7 @@ public function testRule(): void 113, ], [ - 'Trying to invoke array(object, \'bar\') but it might not be a callable.', + 'Trying to invoke array{object, \'bar\'} but it might not be a callable.', 131, ], [ @@ -122,15 +128,15 @@ public function testRule(): void 148, ], [ - 'Trying to invoke array(object, \'yo\') but it might not be a callable.', + 'Trying to invoke array{object, \'yo\'} but it might not be a callable.', 163, ], [ - 'Trying to invoke array(object, \'yo\') but it might not be a callable.', + 'Trying to invoke array{object, \'yo\'} but it might not be a callable.', 167, ], [ - 'Trying to invoke array(\'CallCallables\\\\CallableInForeach\', \'bar\'|\'foo\') but it might not be a callable.', + 'Trying to invoke array{\'CallCallables\\\\CallableInForeach\', \'bar\'|\'foo\'} but it might not be a callable.', 179, ], ]); @@ -166,4 +172,94 @@ public function testNamedArguments(): void ]); } + public function dataBug3566(): array + { + return [ + [ + true, + [ + [ + 'Parameter #1 $ of closure expects int, TMemberType given.', + 29, + ], + ], + ], + [ + false, + [], + ], + ]; + } + + /** + * @dataProvider dataBug3566 + * @param mixed[] $errors + */ + public function testBug3566(bool $checkExplicitMixed, array $errors): void + { + $this->checkExplicitMixed = $checkExplicitMixed; + $this->analyse([__DIR__ . '/data/bug-3566.php'], $errors); + } + + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/callables-nullsafe.php'], [ + [ + 'Parameter #1 $val of closure expects int, int|null given.', + 18, + ], + ]); + } + + public function testBug1849(): void + { + $this->analyse([__DIR__ . '/data/bug-1849.php'], []); + } + + public function testFirstClassCallables(): void + { + if (PHP_VERSION_ID < 80100 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/call-first-class-callables.php'], [ + [ + 'Unable to resolve the template type T in call to closure', + 14, + 'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type', + ], + [ + 'Unable to resolve the template type T in call to closure', + 17, + 'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type', + ], + ]); + } + + public function testBug6701(): void + { + if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + $this->analyse([__DIR__ . '/data/bug-6701.php'], [ + [ + 'Parameter #1 $test of closure expects string|null, int given.', + 14, + ], + [ + 'Parameter #1 $test of closure expects string|null, int given.', + 18, + ], + [ + 'Parameter #1 $test of closure expects string|null, int given.', + 24, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 5fa75fb444..35d1e30694 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -6,21 +6,26 @@ use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class CallToFunctionParametersRuleTest extends \PHPStan\Testing\RuleTestCase +class CallToFunctionParametersRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + private bool $checkExplicitMixed = false; + + protected function getRule(): Rule { $broker = $this->createReflectionProvider(); return new CallToFunctionParametersRule( $broker, - new FunctionCallParametersCheck(new RuleLevelHelper($broker, true, false, true, false), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(true), true, true, true, true, true) + new FunctionCallParametersCheck(new RuleLevelHelper($broker, true, false, true, $this->checkExplicitMixed), new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), ); } @@ -32,6 +37,10 @@ public function testCallToFunctionWithoutParameters(): void public function testCallToFunctionWithIncorrectParameters(): void { + $setErrorHandlerError = PHP_VERSION_ID < 80000 + ? 'Parameter #1 $callback of function set_error_handler expects (callable(int, string, string, int, array): bool)|null, Closure(mixed, mixed, mixed, mixed): void given.' + : 'Parameter #1 $callback of function set_error_handler expects (callable(int, string, string, int): bool)|null, Closure(mixed, mixed, mixed, mixed): void given.'; + require_once __DIR__ . '/data/incorrect-call-to-function-definition.php'; $this->analyse([__DIR__ . '/data/incorrect-call-to-function.php'], [ [ @@ -47,7 +56,7 @@ public function testCallToFunctionWithIncorrectParameters(): void 14, ], [ - 'Parameter #1 $callback of function set_error_handler expects (callable(int, string, string, int, array): bool)|null, Closure(mixed, mixed, mixed, mixed): void given.', + $setErrorHandlerError, 16, ], ]); @@ -290,7 +299,7 @@ public function testPassingNonVariableToParameterPassedByReference(): void 33, ], [ - 'Parameter #1 $array of function reset expects array, null given.', + 'Parameter #1 $array of function reset expects array|object, null given.', 39, ], ]); @@ -318,7 +327,7 @@ public function testImplodeOnPhp74(): void if (PHP_VERSION_ID >= 80000) { $errors = [ [ - 'Parameter #2 $array of function implode expects array, string given.', + 'Parameter #2 $array of function implode expects array|null, string given.', 8, ], ]; @@ -337,7 +346,7 @@ public function testImplodeOnLessThanPhp74(): void if (PHP_VERSION_ID >= 80000) { $errors = [ [ - 'Parameter #2 $array of function implode expects array, string given.', + 'Parameter #2 $array of function implode expects array|null, string given.', 8, ], ]; @@ -563,11 +572,11 @@ public function testArrayReduceCallback(): void 5, ], [ - 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): string given.', + 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): non-empty-string given.', 13, ], [ - 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): string given.', + 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): non-empty-string given.', 22, ], ]); @@ -584,11 +593,11 @@ public function testArrayReduceArrowFunctionCallback(): void 5, ], [ - 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): string given.', + 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): non-empty-string given.', 11, ], [ - 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): string given.', + 'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): non-empty-string given.', 18, ], ]); @@ -625,18 +634,33 @@ public function testArrayWalkArrowFunctionCallback(): void ]); } - public function testUasortCallback(): void + public function testPregReplaceCallback(): void { - $paramTwoName = PHP_VERSION_ID >= 80000 - ? 'callback' - : 'cmp_function'; + $this->analyse([__DIR__ . '/data/preg_replace_callback.php'], [ + [ + 'Parameter #2 $callback of function preg_replace_callback expects callable(array): string, Closure(string): string given.', + 6, + ], + [ + 'Parameter #2 $callback of function preg_replace_callback expects callable(array): string, Closure(string): string given.', + 13, + ], + [ + 'Parameter #2 $callback of function preg_replace_callback expects callable(array): string, Closure(array): void given.', + 20, + ], + [ + 'Parameter #2 $callback of function preg_replace_callback expects callable(array): string, Closure(): void given.', + 25, + ], + ]); + } + public function testUasortCallback(): void + { $this->analyse([__DIR__ . '/data/uasort.php'], [ [ - sprintf( - 'Parameter #2 $%s of function uasort expects callable(int, int): int, Closure(string, string): 1 given.', - $paramTwoName - ), + 'Parameter #2 $callback of function uasort expects callable(int, int): int, Closure(string, string): 1 given.', 7, ], ]); @@ -647,16 +671,10 @@ public function testUasortArrowFunctionCallback(): void if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { $this->markTestSkipped('Test requires PHP 7.4.'); } - $paramTwoName = PHP_VERSION_ID >= 80000 - ? 'callback' - : 'cmp_function'; $this->analyse([__DIR__ . '/data/uasort_arrow.php'], [ [ - sprintf( - 'Parameter #2 $%s of function uasort expects callable(int, int): int, Closure(string, string): 1 given.', - $paramTwoName - ), + 'Parameter #2 $callback of function uasort expects callable(int, int): int, Closure(string, string): 1 given.', 7, ], ]); @@ -664,16 +682,9 @@ public function testUasortArrowFunctionCallback(): void public function testUsortCallback(): void { - $paramTwoName = PHP_VERSION_ID >= 80000 - ? 'callback' - : 'cmp_function'; - $this->analyse([__DIR__ . '/data/usort.php'], [ [ - sprintf( - 'Parameter #2 $%s of function usort expects callable(int, int): int, Closure(string, string): 1 given.', - $paramTwoName - ), + 'Parameter #2 $callback of function usort expects callable(int, int): int, Closure(string, string): 1 given.', 14, ], ]); @@ -685,16 +696,9 @@ public function testUsortArrowFunctionCallback(): void $this->markTestSkipped('Test requires PHP 7.4.'); } - $paramTwoName = PHP_VERSION_ID >= 80000 - ? 'callback' - : 'cmp_function'; - $this->analyse([__DIR__ . '/data/usort_arrow.php'], [ [ - sprintf( - 'Parameter #2 $%s of function usort expects callable(int, int): int, Closure(string, string): 1 given.', - $paramTwoName - ), + 'Parameter #2 $callback of function usort expects callable(int, int): int, Closure(string, string): 1 given.', 14, ], ]); @@ -702,20 +706,13 @@ public function testUsortArrowFunctionCallback(): void public function testUksortCallback(): void { - $paramTwoName = PHP_VERSION_ID >= 80000 - ? 'callback' - : 'cmp_function'; - $this->analyse([__DIR__ . '/data/uksort.php'], [ [ - sprintf( - 'Parameter #2 $%s of function uksort expects callable(string, string): int, Closure(stdClass, stdClass): 1 given.', - $paramTwoName - ), + 'Parameter #2 $callback of function uksort expects callable(string, string): int, Closure(stdClass, stdClass): 1 given.', 14, ], [ - sprintf('Parameter #2 $%s of function uksort expects callable(int, int): int, Closure(string, string): 1 given.', $paramTwoName), + 'Parameter #2 $callback of function uksort expects callable(int, int): int, Closure(string, string): 1 given.', 50, ], ]); @@ -727,20 +724,13 @@ public function testUksortArrowFunctionCallback(): void $this->markTestSkipped('Test requires PHP 7.4.'); } - $paramTwoName = PHP_VERSION_ID >= 80000 - ? 'callback' - : 'cmp_function'; - $this->analyse([__DIR__ . '/data/uksort_arrow.php'], [ [ - sprintf( - 'Parameter #2 $%s of function uksort expects callable(string, string): int, Closure(stdClass, stdClass): 1 given.', - $paramTwoName - ), + 'Parameter #2 $callback of function uksort expects callable(string, string): int, Closure(stdClass, stdClass): 1 given.', 14, ], [ - sprintf('Parameter #2 $%s of function uksort expects callable(int, int): int, Closure(string, string): 1 given.', $paramTwoName), + 'Parameter #2 $callback of function uksort expects callable(int, int): int, Closure(string, string): 1 given.', 44, ], ]); @@ -775,4 +765,206 @@ public function testBug3660(): void ]); } + public function testExplode(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/explode-80.php'], [ + [ + 'Parameter #1 $separator of function explode expects non-empty-string, string given.', + 14, + ], + [ + 'Parameter #1 $separator of function explode expects non-empty-string, \'\' given.', + 16, + ], + [ + 'Parameter #1 $separator of function explode expects non-empty-string, 1 given.', + 17, + ], + ]); + } + + public function testProcOpen(): void + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->analyse([__DIR__ . '/data/proc_open.php'], [ + [ + 'Parameter #1 $command of function proc_open expects array|string, array given.', + 6, + ], + ]); + } + + public function testBug5609(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5609.php'], []); + } + + public function dataArrayMapMultiple(): array + { + return [ + [true], + [false], + ]; + } + + /** + * @dataProvider dataArrayMapMultiple + */ + public function testArrayMapMultiple(bool $checkExplicitMixed): void + { + $this->checkExplicitMixed = $checkExplicitMixed; + $this->analyse([__DIR__ . '/data/array_map_multiple.php'], [ + [ + 'Parameter #1 $callback of function array_map expects (callable(1|2, \'bar\'|\'foo\'): mixed)|null, Closure(int, int): void given.', + 58, + ], + ]); + } + + public function dataArrayFilterCallback(): array + { + return [ + [true], + [false], + ]; + } + + /** + * @dataProvider dataArrayFilterCallback + */ + 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.', + 17, + ], + ]; + if ($checkExplicitMixed) { + $errors[] = [ + 'Parameter #2 $callback of function array_filter expects callable(mixed): mixed, Closure(int): true given.', + 20, + ]; + } + $this->analyse([__DIR__ . '/data/array_filter_callback.php'], $errors); + } + + public function testBug5356(): void + { + if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->analyse([__DIR__ . '/data/bug-5356.php'], [ + [ + 'Parameter #1 $callback of function array_map expects (callable(string): mixed)|null, Closure(array): \'a\' given.', + 13, + ], + [ + 'Parameter #1 $callback of function array_map expects (callable(string): mixed)|null, Closure(array): \'a\' given.', + 21, + ], + ]); + } + + public function testBug1954(): void + { + $this->analyse([__DIR__ . '/data/bug-1954.php'], [ + [ + 'Parameter #1 $callback of function array_map expects (callable(1|stdClass): mixed)|null, Closure(string): string given.', + 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.', + 13, + ], + ]); + } + + public function testBug5661(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5661.php'], []); + } + + public function testBug5872(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5872.php'], [ + [ + 'Parameter #2 $array of function array_map expects array, mixed given.', + 12, + ], + ]); + } + + public function testBug5834(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5834.php'], []); + } + + public function testBug5881(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5881.php'], []); + } + + public function testBug5861(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5861.php'], []); + } + + public function testCallUserFuncArray(): void + { + if (PHP_VERSION_ID >= 80000) { + $errors = []; + } else { + $errors = [ + [ + 'Parameter #2 $parameters of function call_user_func_array expects array, array> given.', + 3, + ], + ]; + } + $this->analyse([__DIR__ . '/data/call-user-func-array.php'], $errors); + } + + public function testFirstClassCallables(): void + { + if (PHP_VERSION_ID < 80100 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + // handled by a different rule + $this->analyse([__DIR__ . '/data/first-class-callables.php'], []); + } + + public function testBug4413(): void + { + require_once __DIR__ . '/data/bug-4413.php'; + $this->analyse([__DIR__ . '/data/bug-4413.php'], [ + [ + 'Parameter #1 $date of function Bug4413\takesDate expects class-string, string given.', + 18, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionStamentWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php similarity index 58% rename from tests/PHPStan/Rules/Functions/CallToFunctionStamentWithoutSideEffectsRuleTest.php rename to tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php index 202ab84ec6..d05508237c 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionStamentWithoutSideEffectsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php @@ -4,16 +4,17 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class CallToFunctionStamentWithoutSideEffectsRuleTest extends RuleTestCase +class CallToFunctionStatementWithoutSideEffectsRuleTest extends RuleTestCase { protected function getRule(): Rule { - return new CallToFunctionStamentWithoutSideEffectsRule($this->createReflectionProvider()); + return new CallToFunctionStatementWithoutSideEffectsRule($this->createReflectionProvider()); } public function testRule(): void @@ -55,4 +56,30 @@ public function testBug4455(): void $this->analyse([__DIR__ . '/data/bug-4455.php'], []); } + public function testFirstClassCallables(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/first-class-callable-function-without-side-effect.php'], [ + [ + 'Call to function mkdir() on a separate line has no effect.', + 12, + ], + [ + 'Call to function strlen() on a separate line has no effect.', + 24, + ], + [ + 'Call to function FirstClassCallableFunctionWithoutSideEffect\foo() on a separate line has no effect.', + 36, + ], + [ + 'Call to function FirstClassCallableFunctionWithoutSideEffect\bar() on a separate line has no effect.', + 49, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php b/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php index 6b9e2cfe01..a29f282847 100644 --- a/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToNonExistentFunctionRuleTest.php @@ -2,13 +2,17 @@ namespace PHPStan\Rules\Functions; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class CallToNonExistentFunctionRuleTest extends \PHPStan\Testing\RuleTestCase +class CallToNonExistentFunctionRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new CallToNonExistentFunctionRule($this->createReflectionProvider(), true); } @@ -97,4 +101,64 @@ public function testMatchExprAnalysis(): void ]); } + public function testCreateFunctionPhp8(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/create_function.php'], [ + [ + 'Function create_function not found.', + 4, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + + public function testCreateFunctionPhp7(): void + { + if (PHP_VERSION_ID >= 80000) { + $this->markTestSkipped('Test requires PHP 7.x.'); + } + + $this->analyse([__DIR__ . '/data/create_function.php'], []); + } + + public function testBug3576(): void + { + $this->analyse([__DIR__ . '/data/bug-3576.php'], [ + [ + 'Function bug3576 not found.', + 14, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function bug3576 not found.', + 17, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function bug3576 not found.', + 26, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function bug3576 not found.', + 29, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function bug3576 not found.', + 38, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Function bug3576 not found.', + 41, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php index eed51d56d6..dadea27325 100644 --- a/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ClosureAttributesRuleTest.php @@ -8,9 +8,11 @@ use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -25,18 +27,18 @@ protected function getRule(): Rule new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true), + new RuleLevelHelper($reflectionProvider, true, false, true, false), new NullsafeCheck(), new PhpVersion(80000), - new UnresolvableTypeHelper(true), + new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), true, true, true, true, - true ), - new ClassCaseSensitivityCheck($reflectionProvider, false) - ) + new ClassCaseSensitivityCheck($reflectionProvider, false), + ), ); } diff --git a/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php index addb8aaf1a..fd3bb65cea 100644 --- a/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ClosureReturnTypeRuleTest.php @@ -3,15 +3,17 @@ namespace PHPStan\Rules\Functions; use PHPStan\Rules\FunctionReturnTypeCheck; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ClosureReturnTypeRuleTest extends \PHPStan\Testing\RuleTestCase +class ClosureReturnTypeRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new ClosureReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false))); } @@ -40,7 +42,7 @@ public function testClosureReturnTypeRule(): void 46, ], [ - 'Anonymous function should return array()|null but empty return statement found.', + 'Anonymous function should return array{}|null but empty return statement found.', 88, ], [ diff --git a/tests/PHPStan/Rules/Functions/ClosureUsesThisRuleTest.php b/tests/PHPStan/Rules/Functions/ClosureUsesThisRuleTest.php deleted file mode 100644 index a84faa310a..0000000000 --- a/tests/PHPStan/Rules/Functions/ClosureUsesThisRuleTest.php +++ /dev/null @@ -1,29 +0,0 @@ - - */ -class ClosureUsesThisRuleTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new ClosureUsesThisRule(); - } - - public function testRule(): void - { - $this->analyse([__DIR__ . '/data/closure-uses-this.php'], [ - [ - 'Anonymous function uses $this assigned to variable $that. Use $this directly in the function body.', - 16, - ], - ]); - } - -} diff --git a/tests/PHPStan/Rules/Functions/DefineParametersRuleTest.php b/tests/PHPStan/Rules/Functions/DefineParametersRuleTest.php new file mode 100644 index 0000000000..d78909627d --- /dev/null +++ b/tests/PHPStan/Rules/Functions/DefineParametersRuleTest.php @@ -0,0 +1,35 @@ + + */ +class DefineParametersRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new DefineParametersRule(new PhpVersion(PHP_VERSION_ID)); + } + + public function testFile(): void + { + if (PHP_VERSION_ID < 80000) { + $this->analyse([__DIR__ . '/data/call-to-define.php'], []); + } else { + $this->analyse([__DIR__ . '/data/call-to-define.php'], [ + [ + 'Argument #3 ($case_insensitive) is ignored since declaration of case-insensitive constants is no longer supported.', + 3, + ], + ]); + } + } + +} diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php index f93f2c077b..b7fedcce95 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php @@ -5,20 +5,23 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\FunctionDefinitionCheck; +use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassesInArrowFunctionTypehintsRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingClassesInArrowFunctionTypehintsRuleTest extends RuleTestCase { - /** @var int */ - private $phpVersionId = PHP_VERSION_ID; + private int $phpVersionId = PHP_VERSION_ID; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { $broker = $this->createReflectionProvider(); - return new ExistingClassesInArrowFunctionTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker), new PhpVersion($this->phpVersionId), true, false, true)); + return new ExistingClassesInArrowFunctionTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker, true), new UnresolvableTypeHelper(), new PhpVersion($this->phpVersionId), true, false)); } public function testRule(): void @@ -28,11 +31,11 @@ public function testRule(): void } $this->analyse([__DIR__ . '/data/arrow-function-typehints.php'], [ [ - 'Parameter $bar of anonymous function has invalid typehint type ArrowFunctionExistingClassesInTypehints\Bar.', + 'Parameter $bar of anonymous function has invalid type ArrowFunctionExistingClassesInTypehints\Bar.', 10, ], [ - 'Return typehint of anonymous function has invalid type ArrowFunctionExistingClassesInTypehints\Baz.', + 'Anonymous function has invalid return type ArrowFunctionExistingClassesInTypehints\Baz.', 10, ], ]); @@ -63,7 +66,6 @@ public function dataNativeUnionTypes(): array /** * @dataProvider dataNativeUnionTypes - * @param int $phpVersionId * @param mixed[] $errors */ public function testNativeUnionTypes(int $phpVersionId, array $errors): void @@ -105,7 +107,6 @@ public function dataRequiredParameterAfterOptional(): array /** * @dataProvider dataRequiredParameterAfterOptional - * @param int $phpVersionId * @param mixed[] $errors */ public function testRequiredParameterAfterOptional(int $phpVersionId, array $errors): void @@ -118,4 +119,47 @@ public function testRequiredParameterAfterOptional(int $phpVersionId, array $err $this->analyse([__DIR__ . '/data/required-parameter-after-optional-arrow.php'], $errors); } + public function dataIntersectionTypes(): array + { + return [ + [80000, []], + [ + 80100, + [ + [ + 'Parameter $a of anonymous function has unresolvable native type.', + 27, + ], + [ + 'Anonymous function has unresolvable native return type.', + 27, + ], + [ + 'Parameter $a of anonymous function has unresolvable native type.', + 29, + ], + [ + 'Anonymous function has unresolvable native return type.', + 29, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataIntersectionTypes + * @param mixed[] $errors + */ + public function testIntersectionTypes(int $phpVersion, array $errors): void + { + if (!self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->phpVersionId = $phpVersion; + + $this->analyse([__DIR__ . '/data/arrow-function-intersection-types.php'], $errors); + } + } diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php index d9364af66b..0508d55708 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php @@ -5,31 +5,34 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\FunctionDefinitionCheck; +use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassesInClosureTypehintsRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingClassesInClosureTypehintsRuleTest extends RuleTestCase { - /** @var int */ - private $phpVersionId = PHP_VERSION_ID; + private int $phpVersionId = PHP_VERSION_ID; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { $broker = $this->createReflectionProvider(); - return new ExistingClassesInClosureTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker), new PhpVersion($this->phpVersionId), true, false, true)); + return new ExistingClassesInClosureTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker, true), new UnresolvableTypeHelper(), new PhpVersion($this->phpVersionId), true, false)); } public function testExistingClassInTypehint(): void { $this->analyse([__DIR__ . '/data/closure-typehints.php'], [ [ - 'Return typehint of anonymous function has invalid type TestClosureFunctionTypehints\NonexistentClass.', + 'Anonymous function has invalid return type TestClosureFunctionTypehints\NonexistentClass.', 10, ], [ - 'Parameter $bar of anonymous function has invalid typehint type TestClosureFunctionTypehints\BarFunctionTypehints.', + 'Parameter $bar of anonymous function has invalid type TestClosureFunctionTypehints\BarFunctionTypehints.', 15, ], [ @@ -41,11 +44,11 @@ public function testExistingClassInTypehint(): void 30, ], [ - 'Parameter $trait of anonymous function has invalid typehint type TestClosureFunctionTypehints\SomeTrait.', + 'Parameter $trait of anonymous function has invalid type TestClosureFunctionTypehints\SomeTrait.', 45, ], [ - 'Return typehint of anonymous function has invalid type TestClosureFunctionTypehints\SomeTrait.', + 'Anonymous function has invalid return type TestClosureFunctionTypehints\SomeTrait.', 50, ], ]); @@ -55,11 +58,11 @@ public function testValidTypehintPhp71(): void { $this->analyse([__DIR__ . '/data/closure-7.1-typehints.php'], [ [ - 'Parameter $bar of anonymous function has invalid typehint type TestClosureFunctionTypehintsPhp71\NonexistentClass.', + 'Parameter $bar of anonymous function has invalid type TestClosureFunctionTypehintsPhp71\NonexistentClass.', 35, ], [ - 'Return typehint of anonymous function has invalid type TestClosureFunctionTypehintsPhp71\NonexistentClass.', + 'Anonymous function has invalid return type TestClosureFunctionTypehintsPhp71\NonexistentClass.', 35, ], ]); @@ -80,7 +83,7 @@ public function testVoidParameterTypehint(): void } $this->analyse([__DIR__ . '/data/void-parameter-typehint.php'], [ [ - 'Parameter $param of anonymous function has invalid typehint type void.', + 'Parameter $param of anonymous function has invalid type void.', 5, ], ]); @@ -111,7 +114,6 @@ public function dataNativeUnionTypes(): array /** * @dataProvider dataNativeUnionTypes - * @param int $phpVersionId * @param mixed[] $errors */ public function testNativeUnionTypes(int $phpVersionId, array $errors): void @@ -153,7 +155,6 @@ public function dataRequiredParameterAfterOptional(): array /** * @dataProvider dataRequiredParameterAfterOptional - * @param int $phpVersionId * @param mixed[] $errors */ public function testRequiredParameterAfterOptional(int $phpVersionId, array $errors): void @@ -162,4 +163,47 @@ public function testRequiredParameterAfterOptional(int $phpVersionId, array $err $this->analyse([__DIR__ . '/data/required-parameter-after-optional-closures.php'], $errors); } + public function dataIntersectionTypes(): array + { + return [ + [80000, []], + [ + 80100, + [ + [ + 'Parameter $a of anonymous function has unresolvable native type.', + 30, + ], + [ + 'Anonymous function has unresolvable native return type.', + 30, + ], + [ + 'Parameter $a of anonymous function has unresolvable native type.', + 35, + ], + [ + 'Anonymous function has unresolvable native return type.', + 35, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataIntersectionTypes + * @param mixed[] $errors + */ + public function testIntersectionTypes(int $phpVersion, array $errors): void + { + if (!self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->phpVersionId = $phpVersion; + + $this->analyse([__DIR__ . '/data/closure-intersection-types.php'], $errors); + } + } diff --git a/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php b/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php index 9200981e8c..8744cf88be 100644 --- a/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php @@ -5,21 +5,23 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\FunctionDefinitionCheck; +use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassesInTypehintsRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingClassesInTypehintsRuleTest extends RuleTestCase { - /** @var int */ - private $phpVersionId = PHP_VERSION_ID; + private int $phpVersionId = PHP_VERSION_ID; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { $broker = $this->createReflectionProvider(); - return new ExistingClassesInTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker), new PhpVersion($this->phpVersionId), true, false, true)); + return new ExistingClassesInTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker, true), new UnresolvableTypeHelper(), new PhpVersion($this->phpVersionId), true, false)); } public function testExistingClassInTypehint(): void @@ -27,15 +29,15 @@ public function testExistingClassInTypehint(): void require_once __DIR__ . '/data/typehints.php'; $this->analyse([__DIR__ . '/data/typehints.php'], [ [ - 'Return typehint of function TestFunctionTypehints\foo() has invalid type TestFunctionTypehints\NonexistentClass.', + 'Function TestFunctionTypehints\foo() has invalid return type TestFunctionTypehints\NonexistentClass.', 15, ], [ - 'Parameter $bar of function TestFunctionTypehints\bar() has invalid typehint type TestFunctionTypehints\BarFunctionTypehints.', + 'Parameter $bar of function TestFunctionTypehints\bar() has invalid type TestFunctionTypehints\BarFunctionTypehints.', 20, ], [ - 'Return typehint of function TestFunctionTypehints\returnParent() has invalid type TestFunctionTypehints\parent.', + 'Function TestFunctionTypehints\returnParent() has invalid return type TestFunctionTypehints\parent.', 33, ], [ @@ -63,27 +65,27 @@ public function testExistingClassInTypehint(): void 56, ], [ - 'Parameter $trait of function TestFunctionTypehints\referencesTraitsInNative() has invalid typehint type TestFunctionTypehints\SomeTrait.', + 'Parameter $trait of function TestFunctionTypehints\referencesTraitsInNative() has invalid type TestFunctionTypehints\SomeTrait.', 61, ], [ - 'Return typehint of function TestFunctionTypehints\referencesTraitsInNative() has invalid type TestFunctionTypehints\SomeTrait.', + 'Function TestFunctionTypehints\referencesTraitsInNative() has invalid return type TestFunctionTypehints\SomeTrait.', 61, ], [ - 'Parameter $trait of function TestFunctionTypehints\referencesTraitsInPhpDoc() has invalid typehint type TestFunctionTypehints\SomeTrait.', + 'Parameter $trait of function TestFunctionTypehints\referencesTraitsInPhpDoc() has invalid type TestFunctionTypehints\SomeTrait.', 70, ], [ - 'Return typehint of function TestFunctionTypehints\referencesTraitsInPhpDoc() has invalid type TestFunctionTypehints\SomeTrait.', + 'Function TestFunctionTypehints\referencesTraitsInPhpDoc() has invalid return type TestFunctionTypehints\SomeTrait.', 70, ], [ - 'Parameter $string of function TestFunctionTypehints\genericClassString() has invalid typehint type TestFunctionTypehints\SomeNonexistentClass.', + 'Parameter $string of function TestFunctionTypehints\genericClassString() has invalid type TestFunctionTypehints\SomeNonexistentClass.', 78, ], [ - 'Parameter $string of function TestFunctionTypehints\genericTemplateClassString() has invalid typehint type TestFunctionTypehints\SomeNonexistentClass.', + 'Parameter $string of function TestFunctionTypehints\genericTemplateClassString() has invalid type TestFunctionTypehints\SomeNonexistentClass.', 87, ], [ @@ -98,15 +100,15 @@ public function testWithoutNamespace(): void require_once __DIR__ . '/data/typehintsWithoutNamespace.php'; $this->analyse([__DIR__ . '/data/typehintsWithoutNamespace.php'], [ [ - 'Return typehint of function fooWithoutNamespace() has invalid type NonexistentClass.', + 'Function fooWithoutNamespace() has invalid return type NonexistentClass.', 13, ], [ - 'Parameter $bar of function barWithoutNamespace() has invalid typehint type BarFunctionTypehints.', + 'Parameter $bar of function barWithoutNamespace() has invalid type BarFunctionTypehints.', 18, ], [ - 'Return typehint of function returnParentWithoutNamespace() has invalid type parent.', + 'Function returnParentWithoutNamespace() has invalid return type parent.', 31, ], [ @@ -134,19 +136,19 @@ public function testWithoutNamespace(): void 54, ], [ - 'Parameter $trait of function referencesTraitsInNativeWithoutNamespace() has invalid typehint type SomeTraitWithoutNamespace.', + 'Parameter $trait of function referencesTraitsInNativeWithoutNamespace() has invalid type SomeTraitWithoutNamespace.', 59, ], [ - 'Return typehint of function referencesTraitsInNativeWithoutNamespace() has invalid type SomeTraitWithoutNamespace.', + 'Function referencesTraitsInNativeWithoutNamespace() has invalid return type SomeTraitWithoutNamespace.', 59, ], [ - 'Parameter $trait of function referencesTraitsInPhpDocWithoutNamespace() has invalid typehint type SomeTraitWithoutNamespace.', + 'Parameter $trait of function referencesTraitsInPhpDocWithoutNamespace() has invalid type SomeTraitWithoutNamespace.', 68, ], [ - 'Return typehint of function referencesTraitsInPhpDocWithoutNamespace() has invalid type SomeTraitWithoutNamespace.', + 'Function referencesTraitsInPhpDocWithoutNamespace() has invalid return type SomeTraitWithoutNamespace.', 68, ], ]); @@ -159,7 +161,7 @@ public function testVoidParameterTypehint(): void } $this->analyse([__DIR__ . '/data/void-parameter-typehint.php'], [ [ - 'Parameter $param of function VoidParameterTypehint\doFoo() has invalid typehint type void.', + 'Parameter $param of function VoidParameterTypehint\doFoo() has invalid type void.', 9, ], ]); @@ -190,7 +192,6 @@ public function dataNativeUnionTypes(): array /** * @dataProvider dataNativeUnionTypes - * @param int $phpVersionId * @param mixed[] $errors */ public function testNativeUnionTypes(int $phpVersionId, array $errors): void @@ -232,13 +233,58 @@ public function dataRequiredParameterAfterOptional(): array /** * @dataProvider dataRequiredParameterAfterOptional - * @param int $phpVersionId * @param mixed[] $errors */ public function testRequiredParameterAfterOptional(int $phpVersionId, array $errors): void { + if (PHP_VERSION_ID >= 80000 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires static reflection on PHP 8.0 and higher.'); + } $this->phpVersionId = $phpVersionId; $this->analyse([__DIR__ . '/data/required-parameter-after-optional.php'], $errors); } + public function dataIntersectionTypes(): array + { + return [ + [80000, []], + [ + 80100, + [ + [ + 'Parameter $a of function FunctionIntersectionTypes\doBar() has unresolvable native type.', + 30, + ], + [ + 'Function FunctionIntersectionTypes\doBar() has unresolvable native return type.', + 30, + ], + [ + 'Parameter $a of function FunctionIntersectionTypes\doBaz() has unresolvable native type.', + 35, + ], + [ + 'Function FunctionIntersectionTypes\doBaz() has unresolvable native return type.', + 35, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataIntersectionTypes + * @param mixed[] $errors + */ + public function testIntersectionTypes(int $phpVersion, array $errors): void + { + if (!self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->phpVersionId = $phpVersion; + + $this->analyse([__DIR__ . '/data/intersection-types.php'], $errors); + } + } diff --git a/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php index 0833136a92..3dd6443b03 100644 --- a/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/FunctionAttributesRuleTest.php @@ -8,9 +8,11 @@ use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -25,18 +27,18 @@ protected function getRule(): Rule new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true), + new RuleLevelHelper($reflectionProvider, true, false, true, false), new NullsafeCheck(), new PhpVersion(80000), - new UnresolvableTypeHelper(true), + new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), true, true, true, true, - true ), - new ClassCaseSensitivityCheck($reflectionProvider, false) - ) + new ClassCaseSensitivityCheck($reflectionProvider, false), + ), ); } diff --git a/tests/PHPStan/Rules/Functions/FunctionCallableRuleTest.php b/tests/PHPStan/Rules/Functions/FunctionCallableRuleTest.php new file mode 100644 index 0000000000..b19b9b0c2b --- /dev/null +++ b/tests/PHPStan/Rules/Functions/FunctionCallableRuleTest.php @@ -0,0 +1,82 @@ + + */ +class FunctionCallableRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + + return new FunctionCallableRule( + $reflectionProvider, + new RuleLevelHelper($reflectionProvider, true, false, true, false), + new PhpVersion(PHP_VERSION_ID), + true, + true, + ); + } + + public function testNotSupportedOnOlderVersions(): void + { + if (PHP_VERSION_ID >= 80100) { + self::markTestSkipped('Test runs on PHP < 8.1.'); + } + if (!self::$useStaticReflectionProvider) { + self::markTestSkipped('Test requires static reflection.'); + } + + $this->analyse([__DIR__ . '/data/function-callable-not-supported.php'], [ + [ + 'First-class callables are supported only on PHP 8.1 and later.', + 10, + ], + ]); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/function-callable.php'], [ + [ + 'Function nonexistent not found.', + 13, + ], + [ + 'Creating callable from string but it might not be a callable.', + 19, + ], + [ + 'Creating callable from 1 but it\'s not a callable.', + 33, + ], + [ + 'Call to function strlen() with incorrect case: StrLen', + 38, + ], + [ + 'Creating callable from 1|(callable(): mixed) but it might not be a callable.', + 47, + ], + [ + 'Creating callable from an unknown class FunctionCallable\Nonexistent.', + 52, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php b/tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php new file mode 100644 index 0000000000..c151af825f --- /dev/null +++ b/tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php @@ -0,0 +1,51 @@ + + */ +class ImplodeFunctionRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $broker = $this->createReflectionProvider(); + return new ImplodeFunctionRule($broker, new RuleLevelHelper($broker, true, false, true, false)); + } + + public function testFile(): void + { + $this->analyse([__DIR__ . '/data/implode.php'], [ + [ + 'Parameter #2 $array of function implode expects array, array|string> given.', + 9, + ], + [ + 'Parameter #1 $array of function implode expects array, array> given.', + 11, + ], + [ + 'Parameter #1 $array of function implode expects array, array> given.', + 12, + ], + [ + 'Parameter #1 $array of function implode expects array, array> given.', + 13, + ], + [ + 'Parameter #2 $array of function implode expects array, array> given.', + 15, + ], + [ + 'Parameter #2 $array of function join expects array, array> given.', + 16, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/IncompatibleDefaultParameterTypeRuleTest.php b/tests/PHPStan/Rules/Functions/IncompatibleDefaultParameterTypeRuleTest.php index 6dd5a8e4be..56870a1d42 100644 --- a/tests/PHPStan/Rules/Functions/IncompatibleDefaultParameterTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/IncompatibleDefaultParameterTypeRuleTest.php @@ -6,7 +6,7 @@ use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class IncompatibleDefaultParameterTypeRuleTest extends RuleTestCase { diff --git a/tests/PHPStan/Rules/Functions/InnerFunctionRuleTest.php b/tests/PHPStan/Rules/Functions/InnerFunctionRuleTest.php index fe6fb38f23..49ca5741bf 100644 --- a/tests/PHPStan/Rules/Functions/InnerFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Functions/InnerFunctionRuleTest.php @@ -2,13 +2,16 @@ namespace PHPStan\Rules\Functions; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class InnerFunctionRuleTest extends \PHPStan\Testing\RuleTestCase +class InnerFunctionRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new InnerFunctionRule(); } diff --git a/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php b/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php index 93bc8df9bd..44967df450 100644 --- a/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php @@ -3,17 +3,19 @@ namespace PHPStan\Rules\Functions; use PHPStan\Rules\MissingTypehintCheck; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class MissingFunctionParameterTypehintRuleTest extends \PHPStan\Testing\RuleTestCase +class MissingFunctionParameterTypehintRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { $broker = $this->createReflectionProvider(); - return new MissingFunctionParameterTypehintRule(new MissingTypehintCheck($broker, true, true, true)); + return new MissingFunctionParameterTypehintRule(new MissingTypehintCheck($broker, true, true, true, [])); } public function testRule(): void @@ -21,15 +23,15 @@ public function testRule(): void require_once __DIR__ . '/data/missing-function-parameter-typehint.php'; $this->analyse([__DIR__ . '/data/missing-function-parameter-typehint.php'], [ [ - 'Function globalFunction() has parameter $b with no typehint specified.', + 'Function globalFunction() has parameter $b with no type specified.', 9, ], [ - 'Function globalFunction() has parameter $c with no typehint specified.', + 'Function globalFunction() has parameter $c with no type specified.', 9, ], [ - 'Function MissingFunctionParameterTypehint\namespacedFunction() has parameter $d with no typehint specified.', + 'Function MissingFunctionParameterTypehint\namespacedFunction() has parameter $d with no type specified.', 24, ], [ diff --git a/tests/PHPStan/Rules/Functions/MissingFunctionReturnTypehintRuleTest.php b/tests/PHPStan/Rules/Functions/MissingFunctionReturnTypehintRuleTest.php index 006902082e..c5c43617c2 100644 --- a/tests/PHPStan/Rules/Functions/MissingFunctionReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Functions/MissingFunctionReturnTypehintRuleTest.php @@ -3,17 +3,19 @@ namespace PHPStan\Rules\Functions; use PHPStan\Rules\MissingTypehintCheck; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class MissingFunctionReturnTypehintRuleTest extends \PHPStan\Testing\RuleTestCase +class MissingFunctionReturnTypehintRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { $broker = $this->createReflectionProvider(); - return new MissingFunctionReturnTypehintRule(new MissingTypehintCheck($broker, true, true, true)); + return new MissingFunctionReturnTypehintRule(new MissingTypehintCheck($broker, true, true, true, [])); } public function testRule(): void @@ -21,11 +23,11 @@ public function testRule(): void require_once __DIR__ . '/data/missing-function-return-typehint.php'; $this->analyse([__DIR__ . '/data/missing-function-return-typehint.php'], [ [ - 'Function globalFunction1() has no return typehint specified.', + 'Function globalFunction1() has no return type specified.', 5, ], [ - 'Function MissingFunctionReturnTypehint\namespacedFunction1() has no return typehint specified.', + 'Function MissingFunctionReturnTypehint\namespacedFunction1() has no return type specified.', 30, ], [ diff --git a/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php index 648835f7a1..d386e3f05e 100644 --- a/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ParamAttributesRuleTest.php @@ -8,9 +8,11 @@ use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -25,18 +27,18 @@ protected function getRule(): Rule new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true), + new RuleLevelHelper($reflectionProvider, true, false, true, false), new NullsafeCheck(), new PhpVersion(80000), - new UnresolvableTypeHelper(true), + new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), true, true, true, true, - true ), - new ClassCaseSensitivityCheck($reflectionProvider, false) - ) + new ClassCaseSensitivityCheck($reflectionProvider, false), + ), ); } diff --git a/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php b/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php index b89c61e71c..d9effb60e0 100644 --- a/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/PrintfParametersRuleTest.php @@ -3,15 +3,17 @@ namespace PHPStan\Rules\Functions; use PHPStan\Php\PhpVersion; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class PrintfParametersRuleTest extends \PHPStan\Testing\RuleTestCase +class PrintfParametersRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new PrintfParametersRule(new PhpVersion(PHP_VERSION_ID)); } diff --git a/tests/PHPStan/Rules/Functions/RandomIntParametersRuleTest.php b/tests/PHPStan/Rules/Functions/RandomIntParametersRuleTest.php index 06d0f8a03a..4b9ab4e23c 100644 --- a/tests/PHPStan/Rules/Functions/RandomIntParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/RandomIntParametersRuleTest.php @@ -2,20 +2,24 @@ namespace PHPStan\Rules\Functions; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_INT_SIZE; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class RandomIntParametersRuleTest extends \PHPStan\Testing\RuleTestCase +class RandomIntParametersRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new RandomIntParametersRule($this->createReflectionProvider(), true); } public function testFile(): void { - $this->analyse([__DIR__ . '/data/random-int.php'], [ + $expectedErrors = [ [ 'Parameter #1 $min (1) of function random_int expects lower number than parameter #2 $max (0).', 8, @@ -52,7 +56,21 @@ public function testFile(): void 'Parameter #1 $min (int<0, 10>) of function random_int expects lower number than parameter #2 $max (int<0, 10>).', 31, ], - ]); + ]; + if (PHP_INT_SIZE === 4) { + // TODO: should fail on 64-bit in a similar fashion, guess it does not because of the union type + $expectedErrors[] = [ + 'Parameter #1 $min (2147483647) of function random_int expects lower number than parameter #2 $max (-2147483648).', + 33, + ]; + } + + $this->analyse([__DIR__ . '/data/random-int.php'], $expectedErrors); + } + + public function testBug6361(): void + { + $this->analyse([__DIR__ . '/data/bug-6361.php'], []); } } diff --git a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php index 9425f462b6..c31f70b445 100644 --- a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php @@ -3,18 +3,19 @@ namespace PHPStan\Rules\Functions; use PHPStan\Rules\FunctionReturnTypeCheck; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ReturnTypeRuleTest extends \PHPStan\Testing\RuleTestCase +class ReturnTypeRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - [, $functionReflector] = self::getReflectors(); - return new ReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false)), $functionReflector); + return new ReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false))); } public function testReturnTypeRule(): void @@ -86,12 +87,7 @@ public function testIsGenerator(): void public function testBug2568(): void { require_once __DIR__ . '/data/bug-2568.php'; - $this->analyse([__DIR__ . '/data/bug-2568.php'], [ - [ - 'Function Bug2568\my_array_keys() should return array but returns array.', - 12, - ], - ]); + $this->analyse([__DIR__ . '/data/bug-2568.php'], []); } public function testBug2723(): void @@ -105,4 +101,14 @@ public function testBug2723(): void ]); } + public function testBug5706(): void + { + $this->analyse([__DIR__ . '/data/bug-5706.php'], []); + } + + public function testBug5844(): void + { + $this->analyse([__DIR__ . '/data/bug-5844.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/UnusedClosureUsesRuleTest.php b/tests/PHPStan/Rules/Functions/UnusedClosureUsesRuleTest.php index 8152274a6a..0d033268d8 100644 --- a/tests/PHPStan/Rules/Functions/UnusedClosureUsesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/UnusedClosureUsesRuleTest.php @@ -2,15 +2,17 @@ namespace PHPStan\Rules\Functions; +use PHPStan\Rules\Rule; use PHPStan\Rules\UnusedFunctionParametersCheck; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class UnusedClosureUsesRuleTest extends \PHPStan\Testing\RuleTestCase +class UnusedClosureUsesRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new UnusedClosureUsesRule(new UnusedFunctionParametersCheck($this->createReflectionProvider())); } diff --git a/tests/PHPStan/Rules/Functions/data/array_filter_callback.php b/tests/PHPStan/Rules/Functions/data/array_filter_callback.php new file mode 100644 index 0000000000..96933c7e1a --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_filter_callback.php @@ -0,0 +1,25 @@ + + */ + protected $items = []; + + /** + * @param array $items + * @return void + */ + public function __construct($items) + { + $this->items = $items; + } + + /** + * @template TMapValue + * + * @param callable(TValue, TKey): TMapValue $callback + * @return self + */ + public function map(callable $callback) + { + $keys = array_keys($this->items); + + $items = array_map($callback, $this->items, $keys); + + return new self(array_combine($keys, $items)); + } + + /** + * @return array + */ + public function all() + { + return $this->items; + } +} + +class Foo +{ + + public function doFoo(): void + { + array_map(function (int $a, string $b) { + + }, [1, 2], ['foo', 'bar']); + + array_map(function (int $a, int $b) { + + }, [1, 2], ['foo', 'bar']); + } + + public function arrayMapNull(): void + { + array_map(null, [1, 2], [3, 4]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/array_walk.php b/tests/PHPStan/Rules/Functions/data/array_walk.php index 2d513b78ba..eeebab63f0 100644 --- a/tests/PHPStan/Rules/Functions/data/array_walk.php +++ b/tests/PHPStan/Rules/Functions/data/array_walk.php @@ -24,3 +24,17 @@ function(int $value, string $key, int $extra): string { return ''; } ); + +function (): void { + $object = (object)['foo' => 'bar']; + array_walk($object, function ($v) { + return '_' . $v; + }); +}; + +function (): void { + $object = (object)['foo' => 'bar']; + array_walk_recursive($object, function ($v) { + return '_' . $v; + }); +}; diff --git a/tests/PHPStan/Rules/Functions/data/arrow-function-intersection-types.php b/tests/PHPStan/Rules/Functions/data/arrow-function-intersection-types.php new file mode 100644 index 0000000000..246d426fb3 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/arrow-function-intersection-types.php @@ -0,0 +1,29 @@ += 8.1 + +namespace ArrowFunctionIntersectionTypes; + +interface Foo +{ + +} + +interface Bar +{ + +} + +class Lorem +{ + +} + +class Ipsum +{ + +} + +fn(Foo&Bar $a): Foo&Bar => 1; + +fn(Lorem&Ipsum $a): Lorem&Ipsum => 2; + +fn(int&mixed $a): int&mixed => 3; diff --git a/tests/PHPStan/Rules/Functions/data/bug-1849.php b/tests/PHPStan/Rules/Functions/data/bug-1849.php new file mode 100644 index 0000000000..1978c8232f --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-1849.php @@ -0,0 +1,11 @@ + $j ? 1 : -1; + } + ); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-3566.php b/tests/PHPStan/Rules/Functions/data/bug-3566.php new file mode 100644 index 0000000000..6a015f9bfc --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-3566.php @@ -0,0 +1,40 @@ + $array + * @phpstan-param \Closure(TMemberType) : void $validator + */ + public static function validateArrayValueType(array $array, \Closure $validator) : void{ + foreach($array as $k => $v){ + try{ + $validator($v); + }catch(\TypeError $e){ + throw new \TypeError("Incorrect type of element at \"$k\": " . $e->getMessage(), 0, $e); + } + } + } + + /** + * @phpstan-template TMemberType + * @phpstan-param TMemberType $t + * @phpstan-param \Closure(int) : void $validator + */ + public static function doFoo($t, \Closure $validator) : void{ + $validator($t); + } + + /** + * @phpstan-template TMemberType + * @phpstan-param TMemberType $t + * @phpstan-param \Closure(mixed) : void $validator + */ + public static function doFoo2($t, \Closure $validator) : void{ + $validator($t); + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-3576.php b/tests/PHPStan/Rules/Functions/data/bug-3576.php new file mode 100644 index 0000000000..9f73084f22 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-3576.php @@ -0,0 +1,45 @@ + $date + */ +function takesDate(string $date): void {} + +function input(string $in): void { + switch ($in) { + case DateTime::class : + takesDate($in); + break; + case \stdClass::class : + takesDate($in); + break; + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-5356.php b/tests/PHPStan/Rules/Functions/data/bug-5356.php new file mode 100644 index 0000000000..158e2f7532 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5356.php @@ -0,0 +1,24 @@ += 7.4 + +namespace Bug5356; + +class Foo +{ + + public function doFoo(): void + { + /** @var array{name: string, collectors: string[]} $array */ + $array = []; + + array_map(static fn(array $_): string => 'a', $array['collectors']); + } + + public function doBar(): void + { + /** @var array{name: string, collectors: string[]} $array */ + $array = []; + + array_map(static function(array $_): string { return 'a'; }, $array['collectors']); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-5609.php b/tests/PHPStan/Rules/Functions/data/bug-5609.php new file mode 100644 index 0000000000..503f4fe4fb --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5609.php @@ -0,0 +1,18 @@ +entities, function (\stdClass $std): bool { + return true; + }); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-5661.php b/tests/PHPStan/Rules/Functions/data/bug-5661.php new file mode 100644 index 0000000000..471594e367 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5661.php @@ -0,0 +1,24 @@ + $array + */ + function sayHello(array $array): void + { + echo join(', ', $array) . PHP_EOL; + } + + /** + * @param string[] $array + */ + function sayHello2(array $array): void + { + echo join(', ', $array) . PHP_EOL; + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-5706.php b/tests/PHPStan/Rules/Functions/data/bug-5706.php new file mode 100644 index 0000000000..9255da5622 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5706.php @@ -0,0 +1,18 @@ +bar; + } +} + +class Foo +{ + public function getFoo(Bar $bar): void + { + $array = (array) $bar->getBar(); + $statusCode = array_key_exists('key', $array) ? (string) $array['key'] : null; + } +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-5844.php b/tests/PHPStan/Rules/Functions/data/bug-5844.php new file mode 100644 index 0000000000..29313fdd40 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5844.php @@ -0,0 +1,17 @@ + + */ +class FooIterator implements \IteratorAggregate +{ + /** + * @return \Generator + */ + public function getIterator(): \Generator + { + yield 1; + yield 2; + yield 3; + } +} + +function (): void { + \array_map( + static function (int $i): string { + return (string) $i; + }, + \iterator_to_array(new FooIterator()) + ); +}; diff --git a/tests/PHPStan/Rules/Functions/data/bug-5881.php b/tests/PHPStan/Rules/Functions/data/bug-5881.php new file mode 100644 index 0000000000..d586c9e246 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5881.php @@ -0,0 +1,11 @@ + $test ?? ''; + $b(null); + $b($i); + + $c = function ( ?string $test = null ): string { + return $test ?? ''; + }; + $c(null); + $c($i); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/call-first-class-callables.php b/tests/PHPStan/Rules/Functions/data/call-first-class-callables.php new file mode 100644 index 0000000000..4e78b9bb77 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/call-first-class-callables.php @@ -0,0 +1,30 @@ +doBar(...); + $f($mixed); + + $g = \Closure::fromCallable([$this, 'doBar']); + $g($mixed); + } + + /** + * @template T of object + * @param T $object + * @return T + */ + public function doBar($object) + { + return $object; + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/call-to-define.php b/tests/PHPStan/Rules/Functions/data/call-to-define.php new file mode 100644 index 0000000000..fbb38e6306 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/call-to-define.php @@ -0,0 +1,6 @@ + ['bar' => 2]]); diff --git a/tests/PHPStan/Rules/Functions/data/callables-nullsafe.php b/tests/PHPStan/Rules/Functions/data/callables-nullsafe.php new file mode 100644 index 0000000000..8cba81fa7f --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/callables-nullsafe.php @@ -0,0 +1,20 @@ += 8.0 + +namespace CallablesNullsafe; + +class Bar +{ + + public int $val; + +} + +function doFoo(?Bar $bar): void +{ + $fn = function (int $val) { + + }; + + $fn($bar?->val); +} + diff --git a/tests/PHPStan/Rules/Functions/data/closure-intersection-types.php b/tests/PHPStan/Rules/Functions/data/closure-intersection-types.php new file mode 100644 index 0000000000..90051d4342 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/closure-intersection-types.php @@ -0,0 +1,38 @@ += 8.1 + +namespace ClosureIntersectionTypes; + +interface Foo +{ + +} + +interface Bar +{ + +} + +class Lorem +{ + +} + +class Ipsum +{ + +} + +function(Foo&Bar $a): Foo&Bar +{ + +}; + +function(Lorem&Ipsum $a): Lorem&Ipsum +{ + +}; + +function(int&mixed $a): int&mixed +{ + +}; diff --git a/tests/PHPStan/Rules/Functions/data/closure-uses-this.php b/tests/PHPStan/Rules/Functions/data/closure-uses-this.php deleted file mode 100644 index e298303d90..0000000000 --- a/tests/PHPStan/Rules/Functions/data/closure-uses-this.php +++ /dev/null @@ -1,26 +0,0 @@ -= 8.1 + +namespace FirstClassCallableFunctionWithoutSideEffect; + +class Foo +{ + + public static function doFoo(): void + { + $f = mkdir(...); + + mkdir(...); + } + +} + +class Bar +{ + + public static function doFoo(): void + { + $f = strlen(...); + + strlen(...); + } + +} + +function foo(): never +{ + throw new \Exception(); +} + +function (): void { + $f = foo(...); + foo(...); +}; + +/** + * @throws \Exception + */ +function bar() +{ + throw new \Exception(); +} + +function (): void { + $f = bar(...); + bar(...); +}; diff --git a/tests/PHPStan/Rules/Functions/data/first-class-callables.php b/tests/PHPStan/Rules/Functions/data/first-class-callables.php new file mode 100644 index 0000000000..da237d1130 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/first-class-callables.php @@ -0,0 +1,13 @@ += 8.1 + +namespace FirstClassFunctionCallable; + +class Foo +{ + + public function doFoo(): void + { + $f = json_encode(...); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/function-callable-not-supported.php b/tests/PHPStan/Rules/Functions/data/function-callable-not-supported.php new file mode 100644 index 0000000000..534baa8174 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/function-callable-not-supported.php @@ -0,0 +1,13 @@ += 8.1 + +namespace FunctionCallableNotSupported; + +class Foo +{ + + public function doFoo(): void + { + $f = json_encode(...); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/function-callable.php b/tests/PHPStan/Rules/Functions/data/function-callable.php new file mode 100644 index 0000000000..8fe1d46880 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/function-callable.php @@ -0,0 +1,55 @@ += 8.1 + +namespace FunctionCallable; + +use function function_exists; + +class Foo +{ + + public function doFoo(string $s): void + { + strlen(...); + nonexistent(...); + + if (function_exists('blabla')) { + blabla(...); + } + + $s(...); + if (function_exists($s)) { + $s(...); + } + } + + public function doBar(): void + { + $f = function (): void { + + }; + $f(...); + + $i = 1; + $i(...); + } + + public function doBaz(): void + { + StrLen(...); + } + + public function doLorem(callable $cb): void + { + if (rand(0, 1)) { + $cb = 1; + } + + $f = $cb(...); + } + + public function doIpsum(Nonexistent $obj): void + { + $f = $obj(...); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/implode.php b/tests/PHPStan/Rules/Functions/data/implode.php new file mode 100644 index 0000000000..800a61397d --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/implode.php @@ -0,0 +1,26 @@ += 8.1 + +namespace FunctionIntersectionTypes; + +interface Foo +{ + +} + +interface Bar +{ + +} + +class Lorem +{ + +} + +class Ipsum +{ + +} + +function doFoo(Foo&Bar $a): Foo&Bar +{ + +} + +function doBar(Lorem&Ipsum $a): Lorem&Ipsum +{ + +} + +function doBaz(int&mixed $a): int&mixed +{ + +} diff --git a/tests/PHPStan/Rules/Functions/data/preg_replace_callback.php b/tests/PHPStan/Rules/Functions/data/preg_replace_callback.php new file mode 100644 index 0000000000..573275406a --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/preg_replace_callback.php @@ -0,0 +1,50 @@ + 'bogus', 'in' => 'here'], [], $pipes); diff --git a/tests/PHPStan/Rules/Generators/YieldFromTypeRuleTest.php b/tests/PHPStan/Rules/Generators/YieldFromTypeRuleTest.php index 7a949cdc35..09954667f9 100644 --- a/tests/PHPStan/Rules/Generators/YieldFromTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generators/YieldFromTypeRuleTest.php @@ -7,7 +7,7 @@ use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class YieldFromTypeRuleTest extends RuleTestCase { @@ -37,7 +37,7 @@ public function testRule(): void 41, ], [ - 'Generator expects value type array(DateTime, DateTime, stdClass, DateTimeImmutable), array(0 => DateTime, 1 => DateTime, 2 => stdClass, 4 => DateTimeImmutable) given.', + 'Generator expects value type array{DateTime, DateTime, stdClass, DateTimeImmutable}, array{0: DateTime, 1: DateTime, 2: stdClass, 4: DateTimeImmutable} given.', 74, ], [ diff --git a/tests/PHPStan/Rules/Generators/YieldInGeneratorRuleTest.php b/tests/PHPStan/Rules/Generators/YieldInGeneratorRuleTest.php index 95ec184fa4..7e234250d8 100644 --- a/tests/PHPStan/Rules/Generators/YieldInGeneratorRuleTest.php +++ b/tests/PHPStan/Rules/Generators/YieldInGeneratorRuleTest.php @@ -6,7 +6,7 @@ use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class YieldInGeneratorRuleTest extends RuleTestCase { diff --git a/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php b/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php index 58799d2296..15a07d334c 100644 --- a/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php @@ -7,7 +7,7 @@ use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class YieldTypeRuleTest extends RuleTestCase { @@ -45,7 +45,7 @@ public function testRule(): void 17, ], [ - 'Generator expects value type array(0 => DateTime, 1 => DateTime, 2 => stdClass, 4 => DateTimeImmutable), array(DateTime, DateTime, stdClass, DateTimeImmutable) given.', + 'Generator expects value type array{0: DateTime, 1: DateTime, 2: stdClass, 4: DateTimeImmutable}, array{DateTime, DateTime, stdClass, DateTimeImmutable} given.', 25, ], [ diff --git a/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php b/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php index 042f1ebb93..fda1c25d6f 100644 --- a/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php +++ b/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php @@ -4,10 +4,9 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use PHPStan\Type\FileTypeMapper; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class ClassAncestorsRuleTest extends RuleTestCase { @@ -15,13 +14,14 @@ class ClassAncestorsRuleTest extends RuleTestCase protected function getRule(): Rule { return new ClassAncestorsRule( - self::getContainer()->getByType(FileTypeMapper::class), new GenericAncestorsCheck( $this->createReflectionProvider(), new GenericObjectTypeCheck(), new VarianceCheck(), - true - ) + true, + [], + ), + new CrossCheckInterfacesHelper(), ); } @@ -95,6 +95,11 @@ public function testRuleExtends(): void 'Template type T is declared as covariant, but occurs in invariant position in extended type ClassAncestorsExtends\FooGeneric8 of class ClassAncestorsExtends\FooGeneric9.', 192, ], + [ + 'Class ClassAncestorsExtends\FilterIteratorChild extends generic class FilterIterator but does not specify its types: TKey, TValue, TIterator', + 197, + 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', + ], ]); } @@ -204,4 +209,19 @@ public function testBug3922Reversed(): void ]); } + public function testCrossCheckInterfaces(): void + { + $this->analyse([__DIR__ . '/data/cross-check-interfaces.php'], [ + [ + 'Interface IteratorAggregate specifies template type TValue of interface Traversable as string but it\'s already specified as CrossCheckInterfaces\Item.', + 19, + ], + ]); + } + + public function testScalarClassName(): void + { + $this->analyse([__DIR__ . '/data/scalar-class-name.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php index db13da5a03..f9d99309ad 100644 --- a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php @@ -7,7 +7,7 @@ use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class ClassTemplateTypeRuleTest extends RuleTestCase { @@ -20,18 +20,16 @@ protected function getRule(): Rule return new ClassTemplateTypeRule( new TemplateTypeCheck( $broker, - new ClassCaseSensitivityCheck($broker), + new ClassCaseSensitivityCheck($broker, true), new GenericObjectTypeCheck(), $typeAliasResolver, - true - ) + true, + ), ); } public function testRule(): void { - require_once __DIR__ . '/data/class-template.php'; - $this->analyse([__DIR__ . '/data/class-template.php'], [ [ 'PHPDoc tag @template for class ClassTemplateType\Foo cannot have existing class stdClass as its name.', @@ -41,10 +39,6 @@ public function testRule(): void 'PHPDoc tag @template T for class ClassTemplateType\Bar has invalid bound type ClassTemplateType\Zazzzu.', 16, ], - [ - 'PHPDoc tag @template T for class ClassTemplateType\Baz with bound type float is not supported.', - 24, - ], [ 'Class ClassTemplateType\Baz referenced with incorrect case: ClassTemplateType\baz.', 32, @@ -69,10 +63,6 @@ public function testRule(): void 'PHPDoc tag @template T for anonymous class has invalid bound type ClassTemplateType\Zazzzu.', 63, ], - [ - 'PHPDoc tag @template T for anonymous class with bound type float is not supported.', - 68, - ], [ 'Class ClassTemplateType\Baz referenced with incorrect case: ClassTemplateType\baz.', 73, @@ -88,26 +78,36 @@ public function testNestedGenericTypes(): void { $this->analyse([__DIR__ . '/data/nested-generic-types.php'], [ [ - 'Type mixed in generic type NestedGenericTypesClassCheck\SomeObjectInterface in PHPDoc tag @template U is not subtype of template type T of object of class NestedGenericTypesClassCheck\SomeObjectInterface.', + 'Type mixed in generic type NestedGenericTypesClassCheck\SomeObjectInterface in PHPDoc tag @template U is not subtype of template type T of object of interface NestedGenericTypesClassCheck\SomeObjectInterface.', 32, ], [ - 'Type int in generic type NestedGenericTypesClassCheck\SomeObjectInterface in PHPDoc tag @template U is not subtype of template type T of object of class NestedGenericTypesClassCheck\SomeObjectInterface.', + 'Type int in generic type NestedGenericTypesClassCheck\SomeObjectInterface in PHPDoc tag @template U is not subtype of template type T of object of interface NestedGenericTypesClassCheck\SomeObjectInterface.', 41, ], [ - 'PHPDoc tag @template U bound contains generic type NestedGenericTypesClassCheck\NotGeneric but class NestedGenericTypesClassCheck\NotGeneric is not generic.', + 'PHPDoc tag @template U bound contains generic type NestedGenericTypesClassCheck\NotGeneric but interface NestedGenericTypesClassCheck\NotGeneric is not generic.', 52, ], [ - 'PHPDoc tag @template V bound has type NestedGenericTypesClassCheck\MultipleGenerics which does not specify all template types of class NestedGenericTypesClassCheck\MultipleGenerics: T, U', + 'PHPDoc tag @template V bound has type NestedGenericTypesClassCheck\MultipleGenerics which does not specify all template types of interface NestedGenericTypesClassCheck\MultipleGenerics: T, U', 52, ], [ - 'PHPDoc tag @template W bound has type NestedGenericTypesClassCheck\MultipleGenerics which specifies 3 template types, but class NestedGenericTypesClassCheck\MultipleGenerics supports only 2: T, U', + 'PHPDoc tag @template W bound has type NestedGenericTypesClassCheck\MultipleGenerics which specifies 3 template types, but interface NestedGenericTypesClassCheck\MultipleGenerics supports only 2: T, U', 52, ], ]); } + public function testBug5446(): void + { + $this->analyse([__DIR__ . '/data/bug-5446.php'], []); + } + + public function testInInterface(): void + { + $this->analyse([__DIR__ . '/data/interface-template.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Generics/EnumAncestorsRuleTest.php b/tests/PHPStan/Rules/Generics/EnumAncestorsRuleTest.php new file mode 100644 index 0000000000..22524bd2ce --- /dev/null +++ b/tests/PHPStan/Rules/Generics/EnumAncestorsRuleTest.php @@ -0,0 +1,74 @@ + + */ +class EnumAncestorsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new EnumAncestorsRule( + new GenericAncestorsCheck( + $this->createReflectionProvider(), + new GenericObjectTypeCheck(), + new VarianceCheck(), + true, + [], + ), + new CrossCheckInterfacesHelper(), + ); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/enum-ancestors.php'], [ + [ + 'Enum EnumGenericAncestors\Foo has @implements tag, but does not implement any interface.', + 22, + ], + [ + 'PHPDoc tag @implements contains generic type EnumGenericAncestors\NonGeneric but interface EnumGenericAncestors\NonGeneric is not generic.', + 35, + ], + [ + 'Enum EnumGenericAncestors\Foo4 implements generic interface EnumGenericAncestors\Generic but does not specify its types: T, U', + 40, + 'You can turn this off by setting checkGenericClassInNonGenericObjectType: false in your %configurationFile%.', + ], + [ + 'Generic type EnumGenericAncestors\Generic in PHPDoc tag @implements does not specify all template types of interface EnumGenericAncestors\Generic: T, U', + 56, + ], + [ + 'Enum EnumGenericAncestors\Foo7 has @extends tag, but cannot extend anything.', + 64, + ], + ]); + } + + public function testCrossCheckInterfaces(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/cross-check-interfaces-enums.php'], [ + [ + 'Interface IteratorAggregate specifies template type TValue of interface Traversable as string but it\'s already specified as CrossCheckInterfacesEnums\Item.', + 19, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Generics/EnumTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/EnumTemplateTypeRuleTest.php new file mode 100644 index 0000000000..dbd82cf985 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/EnumTemplateTypeRuleTest.php @@ -0,0 +1,38 @@ + + */ +class EnumTemplateTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new EnumTemplateTypeRule(); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/enum-template.php'], [ + [ + 'Enum EnumTemplate\Foo has PHPDoc @template tag but enums cannot be generic.', + 8, + ], + [ + 'Enum EnumTemplate\Bar has PHPDoc @template tags but enums cannot be generic.', + 17, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Generics/FunctionSignatureVarianceRuleTest.php b/tests/PHPStan/Rules/Generics/FunctionSignatureVarianceRuleTest.php index 015bdb9333..f80773321a 100644 --- a/tests/PHPStan/Rules/Generics/FunctionSignatureVarianceRuleTest.php +++ b/tests/PHPStan/Rules/Generics/FunctionSignatureVarianceRuleTest.php @@ -6,7 +6,7 @@ use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class FunctionSignatureVarianceRuleTest extends RuleTestCase { @@ -14,7 +14,7 @@ class FunctionSignatureVarianceRuleTest extends RuleTestCase protected function getRule(): Rule { return new FunctionSignatureVarianceRule( - self::getContainer()->getByType(VarianceCheck::class) + self::getContainer()->getByType(VarianceCheck::class), ); } diff --git a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php index 7f3975cf63..22cb75b8b4 100644 --- a/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/FunctionTemplateTypeRuleTest.php @@ -8,7 +8,7 @@ use PHPStan\Type\FileTypeMapper; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class FunctionTemplateTypeRuleTest extends RuleTestCase { @@ -20,7 +20,7 @@ protected function getRule(): Rule return new FunctionTemplateTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), new GenericObjectTypeCheck(), $typeAliasResolver, true) + new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker, true), new GenericObjectTypeCheck(), $typeAliasResolver, true), ); } @@ -35,14 +35,18 @@ public function testRule(): void 'PHPDoc tag @template T for function FunctionTemplateType\bar() has invalid bound type FunctionTemplateType\Zazzzu.', 16, ], - [ - 'PHPDoc tag @template T for function FunctionTemplateType\baz() with bound type float is not supported.', - 24, - ], [ 'PHPDoc tag @template for function FunctionTemplateType\lorem() cannot have existing type alias TypeAlias as its name.', 32, ], + [ + 'PHPDoc tag @template T for function FunctionTemplateType\resourceBound() with bound type resource is not supported.', + 50, + ], + [ + 'PHPDoc tag @template T for function FunctionTemplateType\nullNotSupported() with bound type null is not supported.', + 68, + ], ]); } diff --git a/tests/PHPStan/Rules/Generics/InterfaceAncestorsRuleTest.php b/tests/PHPStan/Rules/Generics/InterfaceAncestorsRuleTest.php index 6ffca19004..63855f7c05 100644 --- a/tests/PHPStan/Rules/Generics/InterfaceAncestorsRuleTest.php +++ b/tests/PHPStan/Rules/Generics/InterfaceAncestorsRuleTest.php @@ -4,10 +4,9 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use PHPStan\Type\FileTypeMapper; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class InterfaceAncestorsRuleTest extends RuleTestCase { @@ -15,13 +14,14 @@ class InterfaceAncestorsRuleTest extends RuleTestCase protected function getRule(): Rule { return new InterfaceAncestorsRule( - self::getContainer()->getByType(FileTypeMapper::class), new GenericAncestorsCheck( $this->createReflectionProvider(), new GenericObjectTypeCheck(), new VarianceCheck(), - true - ) + true, + [], + ), + new CrossCheckInterfacesHelper(), ); } @@ -197,4 +197,14 @@ public function testRuleExtends(): void ]); } + public function testCrossCheckInterfaces(): void + { + $this->analyse([__DIR__ . '/data/cross-check-interfaces-interfaces.php'], [ + [ + 'Interface IteratorAggregate specifies template type TValue of interface Traversable as string but it\'s already specified as CrossCheckInterfacesInInterfaces\Item.', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php index 231e2eb0c1..243a7bd69f 100644 --- a/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/InterfaceTemplateTypeRuleTest.php @@ -5,10 +5,9 @@ use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use PHPStan\Type\FileTypeMapper; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class InterfaceTemplateTypeRuleTest extends RuleTestCase { @@ -19,15 +18,12 @@ protected function getRule(): Rule $typeAliasResolver = $this->createTypeAliasResolver(['TypeAlias' => 'int'], $broker); return new InterfaceTemplateTypeRule( - self::getContainer()->getByType(FileTypeMapper::class), - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), new GenericObjectTypeCheck(), $typeAliasResolver, true) + new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker, true), new GenericObjectTypeCheck(), $typeAliasResolver, true), ); } public function testRule(): void { - require_once __DIR__ . '/data/interface-template.php'; - $this->analyse([__DIR__ . '/data/interface-template.php'], [ [ 'PHPDoc tag @template for interface InterfaceTemplateType\Foo cannot have existing class stdClass as its name.', @@ -37,10 +33,6 @@ public function testRule(): void 'PHPDoc tag @template T for interface InterfaceTemplateType\Bar has invalid bound type InterfaceTemplateType\Zazzzu.', 16, ], - [ - 'PHPDoc tag @template T for interface InterfaceTemplateType\Baz with bound type float is not supported.', - 24, - ], [ 'PHPDoc tag @template for interface InterfaceTemplateType\Lorem cannot have existing type alias TypeAlias as its name.', 33, @@ -56,4 +48,9 @@ public function testRule(): void ]); } + public function testInClass(): void + { + $this->analyse([__DIR__ . '/data/class-template.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php b/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php index 3facb614dd..b0b64eb766 100644 --- a/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php +++ b/tests/PHPStan/Rules/Generics/MethodSignatureVarianceRuleTest.php @@ -6,7 +6,7 @@ use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class MethodSignatureVarianceRuleTest extends RuleTestCase { @@ -14,7 +14,7 @@ class MethodSignatureVarianceRuleTest extends RuleTestCase protected function getRule(): Rule { return new MethodSignatureVarianceRule( - self::getContainer()->getByType(VarianceCheck::class) + self::getContainer()->getByType(VarianceCheck::class), ); } diff --git a/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php index d8f2c90422..c46eb033db 100644 --- a/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/MethodTemplateTypeRuleTest.php @@ -8,7 +8,7 @@ use PHPStan\Type\FileTypeMapper; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class MethodTemplateTypeRuleTest extends RuleTestCase { @@ -20,7 +20,7 @@ protected function getRule(): Rule return new MethodTemplateTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), new GenericObjectTypeCheck(), $typeAliasResolver, true) + new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker, true), new GenericObjectTypeCheck(), $typeAliasResolver, true), ); } @@ -41,10 +41,6 @@ public function testRule(): void 'PHPDoc tag @template T for method MethodTemplateType\Bar::doFoo() shadows @template T of Exception for class MethodTemplateType\Bar.', 37, ], - [ - 'PHPDoc tag @template T for method MethodTemplateType\Baz::doFoo() with bound type float is not supported.', - 50, - ], [ 'PHPDoc tag @template for method MethodTemplateType\Lorem::doFoo() cannot have existing type alias TypeAlias as its name.', 66, diff --git a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php index 5ae1a3b1aa..135d9e78be 100644 --- a/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/TraitTemplateTypeRuleTest.php @@ -8,7 +8,7 @@ use PHPStan\Type\FileTypeMapper; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class TraitTemplateTypeRuleTest extends RuleTestCase { @@ -20,7 +20,7 @@ protected function getRule(): Rule return new TraitTemplateTypeRule( self::getContainer()->getByType(FileTypeMapper::class), - new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker), new GenericObjectTypeCheck(), $typeAliasResolver, true) + new TemplateTypeCheck($broker, new ClassCaseSensitivityCheck($broker, true), new GenericObjectTypeCheck(), $typeAliasResolver, true), ); } @@ -37,10 +37,6 @@ public function testRule(): void 'PHPDoc tag @template T for trait TraitTemplateType\Bar has invalid bound type TraitTemplateType\Zazzzu.', 16, ], - [ - 'PHPDoc tag @template T for trait TraitTemplateType\Baz with bound type float is not supported.', - 24, - ], [ 'PHPDoc tag @template for trait TraitTemplateType\Lorem cannot have existing type alias TypeAlias as its name.', 33, diff --git a/tests/PHPStan/Rules/Generics/UsedTraitsRuleTest.php b/tests/PHPStan/Rules/Generics/UsedTraitsRuleTest.php index 340d663e34..aead3360e2 100644 --- a/tests/PHPStan/Rules/Generics/UsedTraitsRuleTest.php +++ b/tests/PHPStan/Rules/Generics/UsedTraitsRuleTest.php @@ -20,8 +20,9 @@ protected function getRule(): Rule $this->createReflectionProvider(), new GenericObjectTypeCheck(), new VarianceCheck(), - true - ) + true, + [], + ), ); } diff --git a/tests/PHPStan/Rules/Generics/data/bug-5446.php b/tests/PHPStan/Rules/Generics/data/bug-5446.php new file mode 100644 index 0000000000..a059178a71 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/bug-5446.php @@ -0,0 +1,25 @@ + + * @template Fourth of \Bug5446\D + */ +class X {} diff --git a/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php b/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php index 62a1cd9303..caa906001a 100644 --- a/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php +++ b/tests/PHPStan/Rules/Generics/data/class-ancestors-extends.php @@ -193,3 +193,13 @@ class FooGeneric9 extends FooGeneric8 { } + +class FilterIteratorChild extends \FilterIterator +{ + + public function accept() + { + return true; + } + +} diff --git a/tests/PHPStan/Rules/Generics/data/cross-check-interfaces-enums.php b/tests/PHPStan/Rules/Generics/data/cross-check-interfaces-enums.php new file mode 100644 index 0000000000..d0dac4e481 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/cross-check-interfaces-enums.php @@ -0,0 +1,36 @@ += 8.1 + +namespace CrossCheckInterfacesEnums; + +final class Item +{ +} + +/** + * @extends \Traversable + */ +interface ItemListInterface extends \Traversable +{ +} + +/** + * @implements \IteratorAggregate + */ +enum ItemList implements \IteratorAggregate, ItemListInterface +{ + public function getIterator(): \Traversable + { + return new \ArrayIterator([]); + } +} + +/** + * @implements \IteratorAggregate + */ +enum ItemList2 implements \IteratorAggregate, ItemListInterface +{ + public function getIterator(): \Traversable + { + return new \ArrayIterator([]); + } +} diff --git a/tests/PHPStan/Rules/Generics/data/cross-check-interfaces-interfaces.php b/tests/PHPStan/Rules/Generics/data/cross-check-interfaces-interfaces.php new file mode 100644 index 0000000000..8900cefd5c --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/cross-check-interfaces-interfaces.php @@ -0,0 +1,48 @@ + + */ +interface ItemListInterface extends \Traversable +{ +} + +/** + * @extends \IteratorAggregate + */ +interface ItemList extends \IteratorAggregate, ItemListInterface +{ + +} + +/** + * @extends \IteratorAggregate + */ +interface ItemList2 extends \IteratorAggregate, ItemListInterface +{ + +} + +interface ItemList3 extends ItemList // do not report +{ + +} + +/** + * @extends \Traversable + */ +interface ResultStatement extends \Traversable +{ + +} + +interface Statement extends ResultStatement +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/cross-check-interfaces.php b/tests/PHPStan/Rules/Generics/data/cross-check-interfaces.php new file mode 100644 index 0000000000..76cbd59de8 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/cross-check-interfaces.php @@ -0,0 +1,36 @@ + + */ +interface ItemListInterface extends \Traversable +{ +} + +/** + * @implements \IteratorAggregate + */ +final class ItemList implements \IteratorAggregate, ItemListInterface +{ + public function getIterator(): \Traversable + { + return new \ArrayIterator([]); + } +} + +/** + * @implements \IteratorAggregate + */ +final class ItemList2 implements \IteratorAggregate, ItemListInterface +{ + public function getIterator(): \Traversable + { + return new \ArrayIterator([]); + } +} diff --git a/tests/PHPStan/Rules/Generics/data/enum-ancestors.php b/tests/PHPStan/Rules/Generics/data/enum-ancestors.php new file mode 100644 index 0000000000..01066a0e88 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/enum-ancestors.php @@ -0,0 +1,88 @@ += 8.1 + +namespace EnumGenericAncestors; + +interface NonGeneric +{ + +} + +/** + * @template T of object + * @template U + */ +interface Generic +{ + +} + +/** + * @implements NonGeneric + */ +enum Foo +{ + +} + +enum Foo2 implements NonGeneric +{ + +} + +/** + * @implements NonGeneric + */ +enum Foo3 implements NonGeneric +{ + +} + +enum Foo4 implements Generic +{ + +} + +/** + * @implements Generic<\stdClass, int> + */ +enum Foo5 implements Generic +{ + +} + +/** + * @implements Generic<\stdClass> + */ +enum Foo6 implements Generic +{ + +} + +/** + * @extends Generic<\stdClass, int> + */ +enum Foo7 +{ + +} + +/** + * @extends \Traversable + */ +interface TraversableInt extends \Traversable +{ + +} + +/** + * @implements \IteratorAggregate + */ +enum Foo8 implements TraversableInt, \IteratorAggregate +{ + + public function getIterator() + { + return new \ArrayIterator([]); + } + +} diff --git a/tests/PHPStan/Rules/Generics/data/enum-template.php b/tests/PHPStan/Rules/Generics/data/enum-template.php new file mode 100644 index 0000000000..e55f6fe743 --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/enum-template.php @@ -0,0 +1,25 @@ += 8.1 + +namespace EnumTemplate; + +/** + * @template T + */ +enum Foo +{ + +} + +/** + * @template T + * @template U + */ +enum Bar +{ + +} + +enum Baz +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/function-template.php b/tests/PHPStan/Rules/Generics/data/function-template.php index 994865f88f..7124e4c138 100644 --- a/tests/PHPStan/Rules/Generics/data/function-template.php +++ b/tests/PHPStan/Rules/Generics/data/function-template.php @@ -33,3 +33,45 @@ function lorem() { } + +/** @template T of bool */ +function ipsum() +{ + +} + +/** @template T of float */ +function dolor() +{ + +} + +/** @template T of resource */ +function resourceBound() +{ + +} + +/** @template T of array */ +function izumi() +{ + +} + +/** @template T of array{0: string, 1: bool} */ +function nakano() +{ + +} + +/** @template T of null */ +function nullNotSupported() +{ + +} + +/** @template T of ?int */ +function nullableUnionSupported() +{ + +} diff --git a/tests/PHPStan/Rules/Generics/data/scalar-class-name.php b/tests/PHPStan/Rules/Generics/data/scalar-class-name.php new file mode 100644 index 0000000000..363bbd154e --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/scalar-class-name.php @@ -0,0 +1,56 @@ + + */ +class The implements Scalar +{ + /** + * @var T + */ + private $subject; + + /** + * @var Closure(T): mixed + */ + private $context; + + /** + * @param T $subject + * @param Closure(T): mixed $context + */ + public function __construct( + $subject, + $context + ) + { + $this->subject = $subject; + $this->context = $context; + } + /** + * @return T + */ + public function value() + { + ($this->context)($this->subject); + return $this->subject; + } +} diff --git a/tests/PHPStan/Rules/Keywords/ContinueBreakInLoopRuleTest.php b/tests/PHPStan/Rules/Keywords/ContinueBreakInLoopRuleTest.php index 107d77fb07..4d2e35f59b 100644 --- a/tests/PHPStan/Rules/Keywords/ContinueBreakInLoopRuleTest.php +++ b/tests/PHPStan/Rules/Keywords/ContinueBreakInLoopRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Keywords; +use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; /** @@ -10,7 +11,7 @@ class ContinueBreakInLoopRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new ContinueBreakInLoopRule(); } diff --git a/tests/PHPStan/Rules/Methods/AbstractMethodInNonAbstractClassRuleTest.php b/tests/PHPStan/Rules/Methods/AbstractMethodInNonAbstractClassRuleTest.php index 0717ed9914..36911a51ff 100644 --- a/tests/PHPStan/Rules/Methods/AbstractMethodInNonAbstractClassRuleTest.php +++ b/tests/PHPStan/Rules/Methods/AbstractMethodInNonAbstractClassRuleTest.php @@ -47,7 +47,6 @@ public function testBug3406(): void public function testBug3406ReflectionCheck(): void { - $this->createBroker(); $reflectionProvider = $this->createReflectionProvider(); $reflection = $reflectionProvider->getClass(ClassFoo::class); $this->assertSame(AbstractFoo::class, $reflection->getNativeMethod('myFoo')->getDeclaringClass()->getName()); diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleNoBleedingEdgeTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleNoBleedingEdgeTest.php new file mode 100644 index 0000000000..0272593c76 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleNoBleedingEdgeTest.php @@ -0,0 +1,61 @@ + + */ +class CallMethodsRuleNoBleedingEdgeTest extends RuleTestCase +{ + + private bool $checkExplicitMixed; + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, $this->checkExplicitMixed); + 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), + ); + } + + public function testGenericsInferCollection(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/generics-infer-collection.php'], [ + [ + 'Parameter #1 $c of method GenericsInferCollection\Foo::doBar() expects GenericsInferCollection\ArrayCollection, GenericsInferCollection\ArrayCollection given.', + 43, + ], + ]); + } + + public function testGenericsInferCollectionLevel8(): void + { + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/generics-infer-collection.php'], [ + [ + 'Parameter #1 $c of method GenericsInferCollection\Foo::doBar() expects GenericsInferCollection\ArrayCollection, GenericsInferCollection\ArrayCollection given.', + 43, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + // no bleeding edge + return []; + } + +} diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index c4c98e929b..93d25e5e48 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -6,44 +6,35 @@ use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class CallMethodsRuleTest extends \PHPStan\Testing\RuleTestCase +class CallMethodsRuleTest extends RuleTestCase { - /** @var bool */ - private $checkThisOnly; + private bool $checkThisOnly; - /** @var bool */ - private $checkNullables; + private bool $checkNullables; - /** @var bool */ - private $checkUnionTypes; + private bool $checkUnionTypes; - /** @var bool */ - private $checkExplicitMixed = false; + private bool $checkExplicitMixed = false; - /** @var int */ - private $phpVersion = PHP_VERSION_ID; - - /** @var bool */ - private $checkNeverInGenericReturnType = false; + private int $phpVersion = PHP_VERSION_ID; protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - $ruleLevelHelper = new RuleLevelHelper($broker, $this->checkNullables, $this->checkThisOnly, $this->checkUnionTypes, $this->checkExplicitMixed); + $reflectionProvider = $this->createReflectionProvider(); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, $this->checkNullables, $this->checkThisOnly, $this->checkUnionTypes, $this->checkExplicitMixed); return new CallMethodsRule( - $broker, - new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new PhpVersion($this->phpVersion), new UnresolvableTypeHelper(true), true, true, true, true, $this->checkNeverInGenericReturnType), - $ruleLevelHelper, - true, - true + new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true), + new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new PhpVersion($this->phpVersion), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), true, true, true, true), ); } @@ -393,15 +384,15 @@ public function testCallMethods(): void 914, ], [ - 'Parameter #1 $callable of method Test\MethodExists::doBar() expects callable(): mixed, array(class-string|object, \'foo\') given.', + 'Parameter #1 $callable of method Test\\MethodExists::doBar() expects callable(): mixed, array{class-string|object, \'foo\'} given.', 915, ], [ - 'Parameter #1 $callable of method Test\MethodExists::doBar() expects callable(): mixed, array(class-string|object, \'bar\') given.', + 'Parameter #1 $callable of method Test\\MethodExists::doBar() expects callable(): mixed, array{class-string|object, \'bar\'} given.', 916, ], [ - 'Parameter #1 $callable of method Test\MethodExists::doBar() expects callable(): mixed, array(object, \'bar\') given.', + 'Parameter #1 $callable of method Test\\MethodExists::doBar() expects callable(): mixed, array{object, \'bar\'} given.', 921, ], [ @@ -409,11 +400,11 @@ public function testCallMethods(): void 942, ], [ - 'Parameter #1 $s of method Test\IssetCumulativeArray::doBar() expects string, int given.', + 'Parameter #1 $s of method Test\IssetCumulativeArray::doBar() expects string, int<0, max> given.', 964, ], [ - 'Parameter #1 $s of method Test\IssetCumulativeArray::doBar() expects string, int given.', + 'Parameter #1 $s of method Test\IssetCumulativeArray::doBar() expects string, int<1, max> given.', 987, ], [ @@ -478,21 +469,42 @@ 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 $a of method Test\\CallableWithMixedArray::doBar() expects callable(array): array, Closure(array): array{\'foo\'}|null given.', 1533, ], [ - 'Parameter #1 $members of method Test\ParameterTypeCheckVerbosity::doBar() expects array string, \'code\' => string)>, array string)> given.', + 'Parameter #1 $members of method Test\\ParameterTypeCheckVerbosity::doBar() expects array, array given.', 1589, ], [ - 'Parameter #1 $test of method Test\NumericStringParam::sayHello() expects string&numeric, 123 given.', + 'Parameter #1 $test of method Test\NumericStringParam::sayHello() expects numeric-string, 123 given.', 1657, ], [ - 'Parameter #1 $test of method Test\NumericStringParam::sayHello() expects string&numeric, \'abc\' given.', + 'Parameter #1 $test of method Test\NumericStringParam::sayHello() expects numeric-string, \'abc\' given.', 1658, ], + [ + 'Parameter #1 $date of method Test\HelloWorld3::sayHello() expects array|int, DateTimeInterface given.', + 1732, + ], + [ + 'Parameter #1 $a of method Test\InvalidReturnTypeUsingArrayTemplateTypeBound::bar() expects array, array given.', + 1751, + ], + [ + 'Unable to resolve the template type T in call to method Test\InvalidReturnTypeUsingArrayTemplateTypeBound::bar()', + 1751, + 'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type', + ], + [ + 'Parameter #1 $code of method Test\\KeyOfParam::foo() expects \'jfk\'|\'lga\', \'sfo\' given.', + 1777, + ], + [ + 'Parameter #1 $code of method Test\\ValueOfParam::foo() expects \'John F. Kennedy…\'|\'La Guardia Airport\', \'Newark Liberty…\' given.', + 1802, + ], ]); } @@ -691,23 +703,23 @@ public function testCallMethodsOnThisOnly(): void 867, ], [ - 'Parameter #1 $callable of method Test\MethodExists::doBar() expects callable(): mixed, array(class-string|object, \'foo\') given.', + 'Parameter #1 $callable of method Test\\MethodExists::doBar() expects callable(): mixed, array{class-string|object, \'foo\'} given.', 915, ], [ - 'Parameter #1 $callable of method Test\MethodExists::doBar() expects callable(): mixed, array(class-string|object, \'bar\') given.', + 'Parameter #1 $callable of method Test\\MethodExists::doBar() expects callable(): mixed, array{class-string|object, \'bar\'} given.', 916, ], [ - 'Parameter #1 $callable of method Test\MethodExists::doBar() expects callable(): mixed, array(object, \'bar\') given.', + 'Parameter #1 $callable of method Test\\MethodExists::doBar() expects callable(): mixed, array{object, \'bar\'} given.', 921, ], [ - 'Parameter #1 $s of method Test\IssetCumulativeArray::doBar() expects string, int given.', + 'Parameter #1 $s of method Test\IssetCumulativeArray::doBar() expects string, int<0, max> given.', 964, ], [ - 'Parameter #1 $s of method Test\IssetCumulativeArray::doBar() expects string, int given.', + 'Parameter #1 $s of method Test\IssetCumulativeArray::doBar() expects string, int<1, max> given.', 987, ], [ @@ -752,21 +764,42 @@ 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 $a of method Test\\CallableWithMixedArray::doBar() expects callable(array): array, Closure(array): array{\'foo\'}|null given.', 1533, ], [ - 'Parameter #1 $members of method Test\ParameterTypeCheckVerbosity::doBar() expects array string, \'code\' => string)>, array string)> given.', + 'Parameter #1 $members of method Test\\ParameterTypeCheckVerbosity::doBar() expects array, array given.', 1589, ], [ - 'Parameter #1 $test of method Test\NumericStringParam::sayHello() expects string&numeric, 123 given.', + 'Parameter #1 $test of method Test\NumericStringParam::sayHello() expects numeric-string, 123 given.', 1657, ], [ - 'Parameter #1 $test of method Test\NumericStringParam::sayHello() expects string&numeric, \'abc\' given.', + 'Parameter #1 $test of method Test\NumericStringParam::sayHello() expects numeric-string, \'abc\' given.', 1658, ], + [ + 'Parameter #1 $date of method Test\HelloWorld3::sayHello() expects array|int, DateTimeInterface given.', + 1732, + ], + [ + 'Parameter #1 $a of method Test\InvalidReturnTypeUsingArrayTemplateTypeBound::bar() expects array, array given.', + 1751, + ], + [ + 'Unable to resolve the template type T in call to method Test\InvalidReturnTypeUsingArrayTemplateTypeBound::bar()', + 1751, + 'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type', + ], + [ + 'Parameter #1 $code of method Test\\KeyOfParam::foo() expects \'jfk\'|\'lga\', \'sfo\' given.', + 1777, + ], + [ + 'Parameter #1 $code of method Test\\ValueOfParam::foo() expects \'John F. Kennedy…\'|\'La Guardia Airport\', \'Newark Liberty…\' given.', + 1802, + ], ]); } @@ -840,7 +873,11 @@ public function testClosureBind(): void ], [ 'Call to an undefined method CallClosureBind\Foo::nonexistentMethod().', - 39, + 38, + ], + [ + 'Call to an undefined method CallClosureBind\Foo::nonexistentMethod().', + 44, ], ]); } @@ -1193,7 +1230,6 @@ public function dataIterable(): array /** * @dataProvider dataIterable - * @param bool $checkNullables */ public function testIterables(bool $checkNullables): void { @@ -1423,26 +1459,56 @@ public function testShadowedTraitMethod(): void $this->analyse([__DIR__ . '/data/shadowed-trait-method.php'], []); } - public function testExplicitMixed(): void + public function dataExplicitMixed(): array { - $this->checkThisOnly = false; - $this->checkNullables = true; - $this->checkUnionTypes = true; - $this->checkExplicitMixed = true; - $this->analyse([__DIR__ . '/data/check-explicit-mixed.php'], [ - [ - 'Cannot call method foo() on mixed.', - 17, - ], + return [ [ - 'Parameter #1 $i of method CheckExplicitMixedMethodCall\Bar::doBar() expects int, mixed given.', - 43, + true, + [ + [ + 'Cannot call method foo() on mixed.', + 17, + ], + [ + 'Parameter #1 $i of method CheckExplicitMixedMethodCall\Bar::doBar() expects int, mixed given.', + 43, + ], + [ + 'Parameter #1 $i of method CheckExplicitMixedMethodCall\Bar::doBar() expects int, T given.', + 65, + ], + [ + 'Parameter #1 $cb of method CheckExplicitMixedMethodCall\CallableMixed::doFoo() expects callable(mixed): void, Closure(int): void given.', + 133, + ], + [ + 'Parameter #1 $cb of method CheckExplicitMixedMethodCall\CallableMixed::doBar2() expects callable(): int, Closure(): mixed given.', + 152, + ], + ], ], [ - 'Parameter #1 $i of method CheckExplicitMixedMethodCall\Bar::doBar() expects int, T given.', - 65, + false, + [], ], - ]); + ]; + } + + /** + * @dataProvider dataExplicitMixed + * @param mixed[] $errors + */ + public function testExplicitMixed(bool $checkExplicitMixed, array $errors): void + { + if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = $checkExplicitMixed; + $this->analyse([__DIR__ . '/data/check-explicit-mixed.php'], $errors); } public function testBug3409(): void @@ -1930,7 +1996,6 @@ public function testGenericReturnTypeResolvedToNever(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->checkNeverInGenericReturnType = true; $this->analyse([__DIR__ . '/data/generic-return-type-never.php'], [ [ 'Return type of call to method GenericReturnTypeNever\Foo::doBar() contains unresolvable type.', @@ -1943,23 +2008,392 @@ public function testGenericReturnTypeResolvedToNever(): void ]); } - public function testDoNotReportGenericReturnTypeResolvedToNever(): void + public function testUnableToResolveCallbackParameterType(): void { $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/data/generic-return-type-never.php'], []); + $this->analyse([__DIR__ . '/data/unable-to-resolve-callback-parameter-type.php'], []); } - public function testUnableToResolveCallbackParameterType(): void + public function testBug4083(): void { + if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/data/unable-to-resolve-callback-parameter-type.php'], []); + $this->analyse([__DIR__ . '/data/bug-4083.php'], []); } - public function testBug4083(): void + public function testBug5253(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-5253.php'], []); + } + + public function testBug4844(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-4844.php'], []); + } + + public function testBug5258(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-5258.php'], []); + } + + public function testBug5591(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-5591.php'], []); + } + + public function testGenericObjectLowerBound(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/../../Analyser/data/generic-object-lower-bound.php'], [ + [ + 'Parameter #1 $c of method GenericObjectLowerBound\Foo::doFoo() expects GenericObjectLowerBound\Collection, GenericObjectLowerBound\Collection given.', + 48, + ], + ]); + } + + public function testNonEmptyStringVerbosity(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/non-empty-string-verbosity.php'], [ + [ + 'Parameter #1 $i of method NonEmptyStringVerbosity\Foo::doBar() expects int, string given.', + 13, + ], + ]); + } + + public function testBug5536(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-5536.php'], []); + } + + public function testBug5372(): void + { + if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->checkThisOnly = false; + $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.', + 68, + ], + [ + 'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection, Bug5372\Collection given.', + 72, + ], + /*[ + 'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection, Bug5372\Collection given.', + 85, + ],*/ + ]); + } + + public function testLiteralString(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/literal-string.php'], [ + [ + 'Parameter #1 $s of method LiteralStringMethod\Foo::requireLiteralString() expects literal-string, string given.', + 18, + ], + [ + 'Parameter #1 $s of method LiteralStringMethod\Foo::requireLiteralString() expects literal-string, int given.', + 21, + ], + [ + 'Parameter #1 $s of method LiteralStringMethod\Foo::requireLiteralString() expects literal-string, 1 given.', + 22, + ], + [ + 'Parameter #1 $s of method LiteralStringMethod\Foo::requireLiteralString() expects literal-string, mixed given.', + 25, + ], + [ + 'Parameter #1 $a of method LiteralStringMethod\Foo::requireArrayOfLiteralStrings() expects array, array given.', + 58, + ], + [ + 'Parameter #1 $a of method LiteralStringMethod\Foo::requireArrayOfLiteralStrings() expects array, array given.', + 60, + ], + [ + 'Parameter #1 $s of method LiteralStringMethod\Foo::requireLiteralString() expects literal-string, array given.', + 65, + ], + [ + 'Parameter #1 $s of method LiteralStringMethod\Foo::requireLiteralString() expects literal-string, mixed given.', + 66, + ], + [ + 'Parameter #1 $s of method LiteralStringMethod\Foo::requireLiteralString() expects literal-string, mixed given.', + 67, + ], + ]); + } + + public function testBug3555(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-3555.php'], [ + [ + 'Parameter #1 $arg of method Bug3555\Enum::run() expects 1|2|3|4|5|6|7|8|9, 100 given.', + 28, + ], + ]); + } + + public function testBug3530(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-3530.php'], []); + } + + public function testBug5562(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-5562.php'], []); + } + + public function testBug4211(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-4211.php'], []); + } + + public function testBug3514(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-3514.php'], []); + } + + public function testBug3465(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-3465.php'], []); + } + + public function testBug5868(): 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/bug-5868.php'], [ + [ + 'Cannot call method nullable1() on Bug5868\HelloWorld|null.', + 14, + ], + [ + 'Cannot call method nullable2() on Bug5868\HelloWorld|null.', + 15, + ], + [ + 'Cannot call method nullable3() on Bug5868\HelloWorld|null.', + 16, + ], + ]); + } + + public function testBug5460(): void + { + if (!self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires static reflection.'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-5460.php'], []); + } + + public function testFirstClassCallable(): void + { + if (PHP_VERSION_ID < 80100 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + + // handled by a different rule + $this->analyse([__DIR__ . '/data/first-class-method-callable.php'], []); + } + + public function testEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + + $this->analyse([__DIR__ . '/data/call-method-in-enum.php'], [ + [ + 'Call to an undefined method CallMethodInEnum\Foo::doNonexistent().', + 11, + ], + [ + 'Call to an undefined method CallMethodInEnum\Bar::doNonexistent().', + 22, + ], + ]); + } + + public function testBug6239(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('This test needs PHP 8.0'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/../../Analyser/data/bug-6293.php'], []); + } + + public function testBug6306(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-6306.php'], []); + } + + public function testRectorDoWhileVarIssue(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/rector-do-while-var-issue.php'], [ + [ + 'Parameter #1 $cls of method RectorDoWhileVarIssue\Foo::processCharacterClass() expects string, int|string given.', + 24, + ], + ]); + } + + public function testReadOnlyPropertyPassedByReference(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/readonly-property-passed-by-reference.php'], [ + [ + 'Parameter #1 $param is passed by reference so it does not accept readonly property ReadonlyPropertyPassedByRef\Foo::$bar.', + 15, + ], + [ + 'Parameter $param is passed by reference so it does not accept readonly property ReadonlyPropertyPassedByRef\Foo::$bar.', + 16, + ], + ]); + } + + public function testBug6055(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6055.php'], []); + } + + public function testBug6081(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6081.php'], []); + } + + public function testBug6236(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6236.php'], []); + } + + public function testBug6118(): 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-6118.php'], []); + } + + public function testBug6464(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6464.php'], []); + } + + public function testBug6423(): void { if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { $this->markTestSkipped('Test requires PHP 7.4.'); @@ -1968,7 +2402,66 @@ public function testBug4083(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/data/bug-4083.php'], []); + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6423.php'], []); + } + + public function testBug5869(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5869.php'], []); + } + + public function testGenericsEmptyArray(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/generics-empty-array.php'], []); + } + + public function testGenericsInferCollection(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/generics-infer-collection.php'], [ + [ + 'Parameter #1 $c of method GenericsInferCollection\Foo::doBar() expects GenericsInferCollection\ArrayCollection, GenericsInferCollection\ArrayCollection given.', + 43, + ], + [ + 'Parameter #1 $c of method GenericsInferCollection\Bar::doBar() expects GenericsInferCollection\ArrayCollection2, GenericsInferCollection\ArrayCollection2<(int|string), mixed> given.', + 62, + ], + [ + 'Parameter #1 $c of method GenericsInferCollection\Bar::doBar() expects GenericsInferCollection\ArrayCollection2, GenericsInferCollection\ArrayCollection2<(int|string), mixed> given.', + 63, + ], + [ + 'Parameter #1 $c of method GenericsInferCollection\Bar::doBar() expects GenericsInferCollection\ArrayCollection2, GenericsInferCollection\ArrayCollection2<(int|string), mixed> given.', + 64, + ], + ]); + } + + public function testGenericsInferCollectionLevel8(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/generics-infer-collection.php'], [ + [ + 'Parameter #1 $c of method GenericsInferCollection\Foo::doBar() expects GenericsInferCollection\ArrayCollection, GenericsInferCollection\ArrayCollection given.', + 43, + ], + ]); } } diff --git a/tests/PHPStan/Rules/Methods/CallPrivateMethodThroughStaticRuleTest.php b/tests/PHPStan/Rules/Methods/CallPrivateMethodThroughStaticRuleTest.php new file mode 100644 index 0000000000..25cd02b49b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/CallPrivateMethodThroughStaticRuleTest.php @@ -0,0 +1,29 @@ + + */ +class CallPrivateMethodThroughStaticRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new CallPrivateMethodThroughStaticRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/call-private-method-static.php'], [ + [ + 'Unsafe call to private method CallPrivateMethodThroughStatic\Foo::doBar() through static::.', + 12, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index 34c40280b8..b62f8a8348 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -7,28 +7,29 @@ use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class CallStaticMethodsRuleTest extends \PHPStan\Testing\RuleTestCase +class CallStaticMethodsRuleTest extends RuleTestCase { - /** @var bool */ - private $checkThisOnly; + private bool $checkThisOnly; - protected function getRule(): \PHPStan\Rules\Rule + private bool $checkExplicitMixed = false; + + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - $ruleLevelHelper = new RuleLevelHelper($broker, true, $this->checkThisOnly, true, false); + $reflectionProvider = $this->createReflectionProvider(); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, $this->checkThisOnly, true, $this->checkExplicitMixed); return new CallStaticMethodsRule( - $broker, - new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new PhpVersion(80000), new UnresolvableTypeHelper(true), true, true, true, true, true), - $ruleLevelHelper, - new ClassCaseSensitivityCheck($broker), - true, - true + 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), ); } @@ -225,6 +226,10 @@ public function testCallStaticMethods(): void 328, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], + [ + 'Call to an undefined static method static(CallStaticMethods\CallWithStatic)::nonexistent().', + 344, + ], ]); } @@ -429,10 +434,73 @@ public function testBug1971(): void $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.', + 'Parameter #1 $callback of static method Closure::fromCallable() expects callable(): mixed, array{class-string, \'sayHello2\'} given.', 16, ], ]); } + public function testBug5259(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/bug-5259.php'], []); + } + + public function testBug5536(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/bug-5536.php'], []); + } + + public function testBug4886(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/bug-4886.php'], []); + } + + public function testFirstClassCallables(): void + { + if (PHP_VERSION_ID < 80100 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkThisOnly = false; + + // handled by a different rule + $this->analyse([__DIR__ . '/data/first-class-static-method-callable.php'], []); + } + + public function testBug5893(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5893.php'], []); + } + + public function testBug6249(): void + { + if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + // discussion https://github.com/phpstan/phpstan/discussions/6249 + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6249.php'], []); + } + + public function testBug5749(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5749.php'], []); + } + + public function testBug5757(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5757.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/CallToMethodStamentWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Methods/CallToMethodStatementWithoutSideEffectsRuleTest.php similarity index 67% rename from tests/PHPStan/Rules/Methods/CallToMethodStamentWithoutSideEffectsRuleTest.php rename to tests/PHPStan/Rules/Methods/CallToMethodStatementWithoutSideEffectsRuleTest.php index 01ac6f193d..09a4010d7e 100644 --- a/tests/PHPStan/Rules/Methods/CallToMethodStamentWithoutSideEffectsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallToMethodStatementWithoutSideEffectsRuleTest.php @@ -5,16 +5,17 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class CallToMethodStamentWithoutSideEffectsRuleTest extends RuleTestCase +class CallToMethodStatementWithoutSideEffectsRuleTest extends RuleTestCase { protected function getRule(): Rule { - return new CallToMethodStamentWithoutSideEffectsRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false)); + return new CallToMethodStatementWithoutSideEffectsRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false)); } public function testRule(): void @@ -85,4 +86,26 @@ public function testBug4455(): void $this->analyse([__DIR__ . '/data/bug-4455.php'], []); } + public function testFirstClassCallables(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/first-class-callable-method-without-side-effect.php'], [ + [ + 'Call to method FirstClassCallableMethodWithoutSideEffect\Foo::doFoo() on a separate line has no effect.', + 12, + ], + [ + 'Call to method FirstClassCallableMethodWithoutSideEffect\Bar::doFoo() on a separate line has no effect.', + 36, + ], + [ + 'Call to method FirstClassCallableMethodWithoutSideEffect\Bar::doBar() on a separate line has no effect.', + 39, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/CallToStaticMethodStamentWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php similarity index 63% rename from tests/PHPStan/Rules/Methods/CallToStaticMethodStamentWithoutSideEffectsRuleTest.php rename to tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php index ec5ade3b82..d7588b2ae5 100644 --- a/tests/PHPStan/Rules/Methods/CallToStaticMethodStamentWithoutSideEffectsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php @@ -5,19 +5,20 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class CallToStaticMethodStamentWithoutSideEffectsRuleTest extends RuleTestCase +class CallToStaticMethodStatementWithoutSideEffectsRuleTest extends RuleTestCase { protected function getRule(): Rule { $broker = $this->createReflectionProvider(); - return new CallToStaticMethodStamentWithoutSideEffectsRule( + return new CallToStaticMethodStatementWithoutSideEffectsRule( new RuleLevelHelper($broker, true, false, true, false), - $broker + $broker, ); } @@ -66,4 +67,26 @@ public function testBug4455(): void $this->analyse([__DIR__ . '/data/bug-4455-static.php'], []); } + public function testFirstClassCallables(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/first-class-callable-static-method-without-side-effect.php'], [ + [ + 'Call to static method FirstClassCallableStaticMethodWithoutSideEffect\Foo::doFoo() on a separate line has no effect.', + 12, + ], + [ + 'Call to static method FirstClassCallableStaticMethodWithoutSideEffect\Bar::doFoo() on a separate line has no effect.', + 36, + ], + [ + 'Call to static method FirstClassCallableStaticMethodWithoutSideEffect\Bar::doBar() on a separate line has no effect.', + 39, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php b/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php index db1fd829c3..bced1041d4 100644 --- a/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php @@ -5,21 +5,23 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\FunctionDefinitionCheck; +use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassesInTypehintsRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingClassesInTypehintsRuleTest extends RuleTestCase { - /** @var int */ - private $phpVersionId = PHP_VERSION_ID; + private int $phpVersionId = PHP_VERSION_ID; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { $broker = $this->createReflectionProvider(); - return new ExistingClassesInTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker), new PhpVersion($this->phpVersionId), true, false, true)); + return new ExistingClassesInTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker, true), new UnresolvableTypeHelper(), new PhpVersion($this->phpVersionId), true, false)); } public function testExistingClassInTypehint(): void @@ -29,51 +31,51 @@ public function testExistingClassInTypehint(): void } $this->analyse([__DIR__ . '/data/typehints.php'], [ [ - 'Return typehint of method TestMethodTypehints\FooMethodTypehints::foo() has invalid type TestMethodTypehints\NonexistentClass.', + 'Method TestMethodTypehints\FooMethodTypehints::foo() has invalid return type TestMethodTypehints\NonexistentClass.', 8, ], [ - 'Parameter $bar of method TestMethodTypehints\FooMethodTypehints::bar() has invalid typehint type TestMethodTypehints\BarMethodTypehints.', + 'Parameter $bar of method TestMethodTypehints\FooMethodTypehints::bar() has invalid type TestMethodTypehints\BarMethodTypehints.', 13, ], [ - 'Parameter $bars of method TestMethodTypehints\FooMethodTypehints::lorem() has invalid typehint type TestMethodTypehints\BarMethodTypehints.', + 'Parameter $bars of method TestMethodTypehints\FooMethodTypehints::lorem() has invalid type TestMethodTypehints\BarMethodTypehints.', 28, ], [ - 'Return typehint of method TestMethodTypehints\FooMethodTypehints::lorem() has invalid type TestMethodTypehints\BazMethodTypehints.', + 'Method TestMethodTypehints\FooMethodTypehints::lorem() has invalid return type TestMethodTypehints\BazMethodTypehints.', 28, ], [ - 'Parameter $bars of method TestMethodTypehints\FooMethodTypehints::ipsum() has invalid typehint type TestMethodTypehints\BarMethodTypehints.', + 'Parameter $bars of method TestMethodTypehints\FooMethodTypehints::ipsum() has invalid type TestMethodTypehints\BarMethodTypehints.', 38, ], [ - 'Return typehint of method TestMethodTypehints\FooMethodTypehints::ipsum() has invalid type TestMethodTypehints\BazMethodTypehints.', + 'Method TestMethodTypehints\FooMethodTypehints::ipsum() has invalid return type TestMethodTypehints\BazMethodTypehints.', 38, ], [ - 'Parameter $bars of method TestMethodTypehints\FooMethodTypehints::dolor() has invalid typehint type TestMethodTypehints\BarMethodTypehints.', + 'Parameter $bars of method TestMethodTypehints\FooMethodTypehints::dolor() has invalid type TestMethodTypehints\BarMethodTypehints.', 48, ], [ - 'Return typehint of method TestMethodTypehints\FooMethodTypehints::dolor() has invalid type TestMethodTypehints\BazMethodTypehints.', + 'Method TestMethodTypehints\FooMethodTypehints::dolor() has invalid return type TestMethodTypehints\BazMethodTypehints.', 48, ], [ - 'Parameter $parent of method TestMethodTypehints\FooMethodTypehints::parentWithoutParent() has invalid typehint type parent.', + 'Parameter $parent of method TestMethodTypehints\FooMethodTypehints::parentWithoutParent() has invalid type parent.', 53, ], [ - 'Return typehint of method TestMethodTypehints\FooMethodTypehints::parentWithoutParent() has invalid type parent.', + 'Method TestMethodTypehints\FooMethodTypehints::parentWithoutParent() has invalid return type parent.', 53, ], [ - 'Parameter $parent of method TestMethodTypehints\FooMethodTypehints::phpDocParentWithoutParent() has invalid typehint type parent.', + 'Parameter $parent of method TestMethodTypehints\FooMethodTypehints::phpDocParentWithoutParent() has invalid type parent.', 62, ], [ - 'Return typehint of method TestMethodTypehints\FooMethodTypehints::phpDocParentWithoutParent() has invalid type parent.', + 'Method TestMethodTypehints\FooMethodTypehints::phpDocParentWithoutParent() has invalid return type parent.', 62, ], [ @@ -84,10 +86,18 @@ public function testExistingClassInTypehint(): void 'Class TestMethodTypehints\FooMethodTypehints referenced with incorrect case: TestMethodTypehints\fOOMethodTypehintS.', 67, ], + [ + 'Class stdClass referenced with incorrect case: STDClass.', + 76, + ], [ 'Class TestMethodTypehints\FooMethodTypehints referenced with incorrect case: TestMethodTypehints\fOOMethodTypehints.', 76, ], + [ + 'Class stdClass referenced with incorrect case: stdclass.', + 76, + ], [ 'Class TestMethodTypehints\FooMethodTypehints referenced with incorrect case: TestMethodTypehints\fOOMethodTypehintS.', 76, @@ -109,15 +119,15 @@ public function testExistingClassInTypehint(): void 94, ], [ - 'Parameter $array of method TestMethodTypehints\FooMethodTypehints::unknownTypesInArrays() has invalid typehint type TestMethodTypehints\AnotherNonexistentClass.', + 'Parameter $array of method TestMethodTypehints\FooMethodTypehints::unknownTypesInArrays() has invalid type TestMethodTypehints\AnotherNonexistentClass.', 102, ], [ - 'Parameter $cb of method TestMethodTypehints\CallableTypehints::doFoo() has invalid typehint type TestMethodTypehints\Bla.', + 'Parameter $cb of method TestMethodTypehints\CallableTypehints::doFoo() has invalid type TestMethodTypehints\Bla.', 113, ], [ - 'Parameter $cb of method TestMethodTypehints\CallableTypehints::doFoo() has invalid typehint type TestMethodTypehints\Ble.', + 'Parameter $cb of method TestMethodTypehints\CallableTypehints::doFoo() has invalid type TestMethodTypehints\Ble.', 113, ], [ @@ -131,11 +141,11 @@ public function testExistingClassInIterableTypehint(): void { $this->analyse([__DIR__ . '/data/typehints-iterable.php'], [ [ - 'Parameter $iterable of method TestMethodTypehints\IterableTypehints::doFoo() has invalid typehint type TestMethodTypehints\NonexistentClass.', + 'Parameter $iterable of method TestMethodTypehints\IterableTypehints::doFoo() has invalid type TestMethodTypehints\NonexistentClass.', 11, ], [ - 'Parameter $iterable of method TestMethodTypehints\IterableTypehints::doFoo() has invalid typehint type TestMethodTypehints\AnotherNonexistentClass.', + 'Parameter $iterable of method TestMethodTypehints\IterableTypehints::doFoo() has invalid type TestMethodTypehints\AnotherNonexistentClass.', 11, ], ]); @@ -148,7 +158,7 @@ public function testVoidParameterTypehint(): void } $this->analyse([__DIR__ . '/data/void-parameter-typehint.php'], [ [ - 'Parameter $param of method VoidParameterTypehintMethod\Foo::doFoo() has invalid typehint type void.', + 'Parameter $param of method VoidParameterTypehintMethod\Foo::doFoo() has invalid type void.', 8, ], ]); @@ -183,7 +193,6 @@ public function dataNativeUnionTypes(): array /** * @dataProvider dataNativeUnionTypes - * @param int $phpVersionId * @param mixed[] $errors */ public function testNativeUnionTypes(int $phpVersionId, array $errors): void @@ -197,20 +206,7 @@ public function dataRequiredParameterAfterOptional(): array return [ [ 70400, - PHP_VERSION_ID < 80000 || self::$useStaticReflectionProvider ? [] : [ - [ - 'Required parameter $bar follows optional parameter $foo', - 8, - ], - [ - 'Required parameter $bar follows optional parameter $foo', - 17, - ], - [ - 'Required parameter $bar follows optional parameter $foo', - 21, - ], - ], + [], ], [ 80000, @@ -234,7 +230,6 @@ public function dataRequiredParameterAfterOptional(): array /** * @dataProvider dataRequiredParameterAfterOptional - * @param int $phpVersionId * @param mixed[] $errors */ public function testRequiredParameterAfterOptional(int $phpVersionId, array $errors): void @@ -253,4 +248,61 @@ public function testBug4641(): void ]); } + public function dataIntersectionTypes(): array + { + return [ + [80000, []], + [ + 80100, + [ + [ + 'Parameter $a of method MethodIntersectionTypes\Foo::doBar() has unresolvable native type.', + 33, + ], + [ + 'Method MethodIntersectionTypes\Foo::doBar() has unresolvable native return type.', + 33, + ], + [ + 'Parameter $a of method MethodIntersectionTypes\Foo::doBaz() has unresolvable native type.', + 38, + ], + [ + 'Method MethodIntersectionTypes\Foo::doBaz() has unresolvable native return type.', + 38, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataIntersectionTypes + * @param mixed[] $errors + */ + public function testIntersectionTypes(int $phpVersion, array $errors): void + { + if (!self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->phpVersionId = $phpVersion; + + $this->analyse([__DIR__ . '/data/intersection-types.php'], $errors); + } + + public function testEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/enums-typehints.php'], [ + [ + 'Parameter $int of method EnumsTypehints\Foo::doFoo() has invalid type EnumsTypehints\intt.', + 8, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/IncompatibleDefaultParameterTypeRuleTest.php b/tests/PHPStan/Rules/Methods/IncompatibleDefaultParameterTypeRuleTest.php index cf9640685c..7e106f88c5 100644 --- a/tests/PHPStan/Rules/Methods/IncompatibleDefaultParameterTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/IncompatibleDefaultParameterTypeRuleTest.php @@ -4,9 +4,10 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class IncompatibleDefaultParameterTypeRuleTest extends RuleTestCase { @@ -45,4 +46,36 @@ public function testBug2573(): void $this->analyse([__DIR__ . '/data/bug-2573.php'], []); } + public function testNewInInitializers(): void + { + if (PHP_VERSION_ID < 80100 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/new-in-initializers.php'], [ + [ + 'Default value of the parameter #1 $i (stdClass) of method MethodNewInInitializers\Foo::doFoo() is incompatible with type int.', + 11, + ], + ]); + } + + public function testDefaultValueForPromotedProperty(): void + { + if (!self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires static reflection.'); + } + + $this->analyse([__DIR__ . '/data/default-value-for-promoted-property.php'], [ + [ + 'Default value of the parameter #1 $foo (string) of method DefaultValueForPromotedProperty\Foo::__construct() is incompatible with type int.', + 9, + ], + [ + 'Default value of the parameter #2 $foo (string) of method DefaultValueForPromotedProperty\Foo::__construct() is incompatible with type int.', + 10, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php b/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php index 4639910662..2aec99cf6b 100644 --- a/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodAttributesRuleTest.php @@ -8,9 +8,11 @@ use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -18,6 +20,8 @@ class MethodAttributesRuleTest extends RuleTestCase { + private int $phpVersion; + protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); @@ -25,18 +29,18 @@ protected function getRule(): Rule new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true), + new RuleLevelHelper($reflectionProvider, true, false, true, false), new NullsafeCheck(), - new PhpVersion(80000), - new UnresolvableTypeHelper(true), + new PhpVersion($this->phpVersion), + new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), true, true, true, true, - true ), - new ClassCaseSensitivityCheck($reflectionProvider, false) - ) + new ClassCaseSensitivityCheck($reflectionProvider, false), + ), ); } @@ -46,6 +50,8 @@ public function testRule(): void $this->markTestSkipped('Test requires PHP 8.0.'); } + $this->phpVersion = 80000; + $this->analyse([__DIR__ . '/data/method-attributes.php'], [ [ 'Attribute class MethodAttributes\Foo does not have the method target.', @@ -54,4 +60,14 @@ public function testRule(): void ]); } + public function testBug5898(): void + { + if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->phpVersion = 70400; + $this->analyse([__DIR__ . '/data/bug-5898.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php b/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php new file mode 100644 index 0000000000..87bb854dfb --- /dev/null +++ b/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php @@ -0,0 +1,90 @@ + + */ +class MethodCallableRuleTest extends RuleTestCase +{ + + private int $phpVersion = PHP_VERSION_ID; + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, false); + + return new MethodCallableRule( + new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true), + new PhpVersion($this->phpVersion), + ); + } + + public function testNotSupportedOnOlderVersions(): void + { + if (PHP_VERSION_ID >= 80100) { + self::markTestSkipped('Test runs on PHP < 8.1.'); + } + if (!self::$useStaticReflectionProvider) { + self::markTestSkipped('Test requires static reflection.'); + } + + $this->analyse([__DIR__ . '/data/method-callable-not-supported.php'], [ + [ + 'First-class callables are supported only on PHP 8.1 and later.', + 10, + ], + ]); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/method-callable.php'], [ + [ + 'Call to method MethodCallable\Foo::doFoo() with incorrect case: dofoo', + 11, + ], + [ + 'Call to an undefined method MethodCallable\Foo::doNonexistent().', + 12, + ], + [ + 'Cannot call method doFoo() on int.', + 13, + ], + [ + 'Call to private method doBar() of class MethodCallable\Bar.', + 18, + ], + [ + 'Call to method doFoo() on an unknown class MethodCallable\Nonexistent.', + 23, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Call to private method doFoo() of class MethodCallable\ParentClass.', + 53, + ], + [ + 'Creating callable from a non-native method MethodCallable\Lorem::doBar().', + 66, + ], + [ + 'Creating callable from a non-native method MethodCallable\Ipsum::doBar().', + 85, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php index f42226d2d5..b48c1e4b53 100644 --- a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php @@ -3,26 +3,26 @@ namespace PHPStan\Rules\Methods; use PHPStan\Php\PhpVersion; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class MethodSignatureRuleTest extends \PHPStan\Testing\RuleTestCase +class MethodSignatureRuleTest extends RuleTestCase { - /** @var bool */ - private $reportMaybes; + private bool $reportMaybes; - /** @var bool */ - private $reportStatic; + private bool $reportStatic; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new OverridingMethodRule( new PhpVersion(PHP_VERSION_ID), new MethodSignatureRule($this->reportMaybes, $this->reportStatic), - true + true, ); } @@ -71,7 +71,7 @@ public function testReturnTypeRule(): void 'Parameter #1 $node (PhpParser\Node\Expr\StaticCall) of method MethodSignature\Rule::processNode() should be contravariant with parameter $node (PhpParser\Node) of method MethodSignature\GenericRule::processNode()', 454, ], - ] + ], ); } @@ -116,7 +116,7 @@ public function testReturnTypeRuleTrait(): void 'Return type (MethodSignature\Cat) of method MethodSignature\SubClassUsingTrait::returnTypeTest5() should be compatible with return type (MethodSignature\Dog) of method MethodSignature\BaseInterface::returnTypeTest5()', 103, ], - ] + ], ); } @@ -145,7 +145,7 @@ public function testReturnTypeRuleTraitWithoutMaybes(): void 'Return type (MethodSignature\Cat) of method MethodSignature\SubClassUsingTrait::returnTypeTest5() should be compatible with return type (MethodSignature\Dog) of method MethodSignature\BaseInterface::returnTypeTest5()', 103, ], - ] + ], ); } @@ -174,7 +174,7 @@ public function testReturnTypeRuleWithoutMaybes(): void 'Return type (MethodSignature\Cat) of method MethodSignature\SubClass::returnTypeTest5() should be compatible with return type (MethodSignature\Dog) of method MethodSignature\BaseInterface::returnTypeTest5()', 358, ], - ] + ], ); } @@ -210,8 +210,16 @@ public function testBug3997(): void $this->reportStatic = true; $this->analyse([__DIR__ . '/data/bug-3997.php'], [ [ - 'Return type (string) of method Bug3997\Ipsum::count() should be compatible with return type (int) of method Countable::count()', - 59, + 'Return type (int) of method Bug3997\Baz::count() should be covariant with return type (int<0, max>) of method Countable::count()', + PHP_VERSION_ID >= 80000 || self::$useStaticReflectionProvider ? 35 : 36, + ], + [ + 'Return type (int) of method Bug3997\Lorem::count() should be covariant with return type (int<0, max>) of method Countable::count()', + PHP_VERSION_ID >= 80000 || self::$useStaticReflectionProvider ? 49 : 50, + ], + [ + 'Return type (string) of method Bug3997\Ipsum::count() should be compatible with return type (int<0, max>) of method Countable::count()', + PHP_VERSION_ID >= 80000 || self::$useStaticReflectionProvider ? 63 : 64, ], ]); } @@ -340,4 +348,26 @@ public function testBug4729(): void $this->analyse([__DIR__ . '/data/bug-4729.php'], []); } + public function testBug4854(): void + { + if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/bug-4854.php'], []); + } + + public function testMemcachePoolGet(): void + { + if (!self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires static reflection.'); + } + + $this->reportMaybes = true; + $this->reportStatic = true; + $this->analyse([__DIR__ . '/data/memcache-pool-get.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/MissingMethodImplementationRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodImplementationRuleTest.php index bdd6304f97..8ed66a80ae 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodImplementationRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodImplementationRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -48,4 +49,18 @@ public function testBug3958(): void $this->analyse([__DIR__ . '/data/bug-3958.php'], []); } + public function testEnums(): void + { + if (!self::$useStaticReflectionProvider || PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs static reflection and PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/missing-method-impl-enum.php'], [ + [ + 'Enum MissingMethodImplEnum\Bar contains abstract method doFoo() from interface MissingMethodImplEnum\FooInterface.', + 21, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php index b400fc75b0..28be69c2fa 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php @@ -3,43 +3,43 @@ namespace PHPStan\Rules\Methods; use PHPStan\Rules\MissingTypehintCheck; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class MissingMethodParameterTypehintRuleTest extends \PHPStan\Testing\RuleTestCase +class MissingMethodParameterTypehintRuleTest extends RuleTestCase { - /** @var bool */ - private $deepInspectTypes = false; - - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { $broker = $this->createReflectionProvider(); - return new MissingMethodParameterTypehintRule(new MissingTypehintCheck($broker, true, true, true, [], $this->deepInspectTypes)); + return new MissingMethodParameterTypehintRule(new MissingTypehintCheck($broker, true, true, true, [])); } public function testRule(): void { $errors = [ [ - 'Method MissingMethodParameterTypehint\FooInterface::getFoo() has parameter $p1 with no typehint specified.', + 'Method MissingMethodParameterTypehint\FooInterface::getFoo() has parameter $p1 with no type specified.', 8, ], [ - 'Method MissingMethodParameterTypehint\FooParent::getBar() has parameter $p2 with no typehint specified.', + 'Method MissingMethodParameterTypehint\FooParent::getBar() has parameter $p2 with no type specified.', 15, ], [ - 'Method MissingMethodParameterTypehint\Foo::getFoo() has parameter $p1 with no typehint specified.', + 'Method MissingMethodParameterTypehint\Foo::getFoo() has parameter $p1 with no type specified.', 25, ], [ - 'Method MissingMethodParameterTypehint\Foo::getBar() has parameter $p2 with no typehint specified.', + 'Method MissingMethodParameterTypehint\Foo::getBar() has parameter $p2 with no type specified.', 33, ], [ - 'Method MissingMethodParameterTypehint\Foo::getBaz() has parameter $p3 with no typehint specified.', + 'Method MissingMethodParameterTypehint\Foo::getBaz() has parameter $p3 with no type specified.', 42, ], [ @@ -95,20 +95,8 @@ public function testPromotedProperties(): void ]); } - public function testDoNotDeepInspectTypes(): void - { - $this->analyse([__DIR__ . '/data/deep-inspect-types.php'], [ - [ - 'Method DeepInspectTypes\Foo::doBar() has parameter $bars with generic class DeepInspectTypes\Bar but does not specify its types: T', - 17, - MissingTypehintCheck::TURN_OFF_NON_GENERIC_CHECK_TIP, - ], - ]); - } - public function testDeepInspectTypes(): void { - $this->deepInspectTypes = true; $this->analyse([__DIR__ . '/data/deep-inspect-types.php'], [ [ 'Method DeepInspectTypes\Foo::doFoo() has parameter $foo with no value type specified in iterable type iterable.', @@ -125,8 +113,17 @@ public function testDeepInspectTypes(): void public function testBug3723(): void { - $this->deepInspectTypes = false; $this->analyse([__DIR__ . '/data/bug-3723.php'], []); } + public function testBug6472(): void + { + $this->analyse([__DIR__ . '/data/bug-6472.php'], []); + } + + public function testFilterIteratorChildClass(): void + { + $this->analyse([__DIR__ . '/data/filter-iterator-child-class.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php index 0509242cd2..21e95e34e3 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php @@ -3,36 +3,38 @@ namespace PHPStan\Rules\Methods; use PHPStan\Rules\MissingTypehintCheck; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class MissingMethodReturnTypehintRuleTest extends \PHPStan\Testing\RuleTestCase +class MissingMethodReturnTypehintRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { $broker = $this->createReflectionProvider(); - return new MissingMethodReturnTypehintRule(new MissingTypehintCheck($broker, true, true, true, [], true)); + return new MissingMethodReturnTypehintRule(new MissingTypehintCheck($broker, true, true, true, [])); } public function testRule(): void { $this->analyse([__DIR__ . '/data/missing-method-return-typehint.php'], [ [ - 'Method MissingMethodReturnTypehint\FooInterface::getFoo() has no return typehint specified.', + 'Method MissingMethodReturnTypehint\FooInterface::getFoo() has no return type specified.', 8, ], [ - 'Method MissingMethodReturnTypehint\FooParent::getBar() has no return typehint specified.', + 'Method MissingMethodReturnTypehint\FooParent::getBar() has no return type specified.', 15, ], [ - 'Method MissingMethodReturnTypehint\Foo::getFoo() has no return typehint specified.', + 'Method MissingMethodReturnTypehint\Foo::getFoo() has no return type specified.', 25, ], [ - 'Method MissingMethodReturnTypehint\Foo::getBar() has no return typehint specified.', + 'Method MissingMethodReturnTypehint\Foo::getBar() has no return type specified.', 33, ], [ @@ -69,13 +71,7 @@ public function testArrayTypehintWithoutNullInPhpDoc(): void public function testBug4415(): void { - $this->analyse([__DIR__ . '/data/bug-4415.php'], [ - [ - 'Method Bug4415Rule\CategoryCollection::getIterator() return type has no value type specified in iterable type Iterator.', - 76, - MissingTypehintCheck::TURN_OFF_MISSING_ITERABLE_VALUE_TYPE_TIP, - ], - ]); + $this->analyse([__DIR__ . '/data/bug-4415.php'], []); } public function testBug5089(): void @@ -83,4 +79,9 @@ public function testBug5089(): void $this->analyse([__DIR__ . '/data/bug-5089.php'], []); } + public function testBug5436(): void + { + $this->analyse([__DIR__ . '/data/bug-5436.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/NullsafeMethodCallRuleTest.php b/tests/PHPStan/Rules/Methods/NullsafeMethodCallRuleTest.php index 55fb808a0d..ac0b89323f 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 diff --git a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php index 06083ccdf0..c926d2f7b4 100644 --- a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php +++ b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php @@ -5,6 +5,9 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use function array_filter; +use function array_merge; +use function array_values; use const PHP_VERSION_ID; /** @@ -13,15 +16,14 @@ class OverridingMethodRuleTest extends RuleTestCase { - /** @var int */ - private $phpVersionId; + private int $phpVersionId; protected function getRule(): Rule { return new OverridingMethodRule( new PhpVersion($this->phpVersionId), new MethodSignatureRule(true, true), - false + false, ); } @@ -43,8 +45,6 @@ public function dataOverridingFinalMethod(): array /** * @dataProvider dataOverridingFinalMethod - * @param int $phpVersion - * @param string $contravariantMessage */ public function testOverridingFinalMethod(int $phpVersion, string $contravariantMessage): void { @@ -128,9 +128,7 @@ public function testOverridingFinalMethod(int $phpVersion, string $contravariant ]; if (PHP_VERSION_ID >= 80000) { - $errors = array_values(array_filter($errors, static function (array $error): bool { - return $error[1] !== 125; - })); + $errors = array_values(array_filter($errors, static fn (array $error): bool => $error[1] !== 125)); } $this->phpVersionId = $phpVersion; @@ -221,14 +219,12 @@ public function dataParameterContravariance(): array /** * @dataProvider dataParameterContravariance - * @param string $file - * @param int $phpVersion * @param mixed[] $expectedErrors */ public function testParameterContravariance( string $file, int $phpVersion, - array $expectedErrors + array $expectedErrors, ): void { if (!self::$useStaticReflectionProvider) { @@ -285,12 +281,11 @@ public function dataReturnTypeCovariance(): array /** * @dataProvider dataReturnTypeCovariance - * @param int $phpVersion * @param mixed[] $expectedErrors */ public function testReturnTypeCovariance( int $phpVersion, - array $expectedErrors + array $expectedErrors, ): void { if (!self::$useStaticReflectionProvider) { @@ -303,9 +298,6 @@ public function testReturnTypeCovariance( /** * @dataProvider dataOverridingFinalMethod - * @param int $phpVersion - * @param string $contravariantMessage - * @param string $covariantMessage */ public function testParle(int $phpVersion, string $contravariantMessage, string $covariantMessage): void { @@ -334,7 +326,6 @@ public function testVariadicParameterIsAlwaysOptional(): void /** * @dataProvider dataOverridingFinalMethod - * @param int $phpVersion */ public function testBug3403(int $phpVersion): void { @@ -465,7 +456,6 @@ public function dataLessOverridenParametersWithVariadic(): array /** * @dataProvider dataLessOverridenParametersWithVariadic - * @param int $phpVersionId * @param mixed[] $errors */ public function testLessOverridenParametersWithVariadic(int $phpVersionId, array $errors): void @@ -498,7 +488,6 @@ public function dataParameterTypeWidening(): array /** * @dataProvider dataParameterTypeWidening - * @param int $phpVersionId * @param mixed[] $errors */ public function testParameterTypeWidening(int $phpVersionId, array $errors): void @@ -510,4 +499,63 @@ public function testParameterTypeWidening(int $phpVersionId, array $errors): voi $this->analyse([__DIR__ . '/data/parameter-type-widening.php'], $errors); } + public function testBug4516(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-4516.php'], []); + } + + public function dataTentativeReturnTypes(): array + { + return [ + [70400, []], + [80000, []], + [ + 80100, + [ + [ + '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().', + 40, + 'Make it covariant, or use the #[\ReturnTypeWillChange] attribute to temporarily suppress the error.', + ], + ], + ], + ]; + } + + /** + * @dataProvider dataTentativeReturnTypes + * @param mixed[] $errors + */ + public function testTentativeReturnTypes(int $phpVersionId, array $errors): void + { + if (!self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires static reflection.'); + } + + if (PHP_VERSION_ID < 80100) { + $errors = []; + } + + $this->phpVersionId = $phpVersionId; + $this->analyse([__DIR__ . '/data/tentative-return-types.php'], $errors); + } + + public function testCountableBug(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/countable-bug.php'], []); + } + + public function testBug6264(): void + { + $this->phpVersionId = PHP_VERSION_ID; + $this->analyse([__DIR__ . '/data/bug-6264.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index 2a59ec2ed7..7939a7c5a5 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -3,17 +3,24 @@ namespace PHPStan\Rules\Methods; use PHPStan\Rules\FunctionReturnTypeCheck; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ReturnTypeRuleTest extends \PHPStan\Testing\RuleTestCase +class ReturnTypeRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + private bool $checkExplicitMixed = false; + + private bool $checkUnionTypes = true; + + protected function getRule(): Rule { - return new ReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false))); + return new ReturnTypeRule(new FunctionReturnTypeCheck(new RuleLevelHelper($this->createReflectionProvider(), true, false, $this->checkUnionTypes, $this->checkExplicitMixed))); } public function testReturnTypeRule(): void @@ -177,95 +184,99 @@ public function testReturnTypeRule(): void ], [ 'Method ReturnTypes\ReturnTernary::returnTernary() should return ReturnTypes\Foo but returns false.', - 625, + 627, ], [ 'Method ReturnTypes\TrickyVoid::returnVoidOrInt() should return int|void but returns string.', - 656, + 658, ], [ 'Method ReturnTypes\TernaryWithJsonEncode::toJson() should return string but returns string|false.', - 687, + 689, ], [ 'Method ReturnTypes\AppendedArrayReturnType::foo() should return array but returns array.', - 700, + 702, ], [ 'Method ReturnTypes\AppendedArrayReturnType::bar() should return array but returns array.', - 710, + 712, ], [ 'Method ReturnTypes\WrongMagicMethods::__toString() should return string but returns true.', - 720, + 722, ], [ 'Method ReturnTypes\WrongMagicMethods::__isset() should return bool but returns int.', - 725, + 727, ], [ 'Method ReturnTypes\WrongMagicMethods::__destruct() with return type void returns int but should not return anything.', - 730, + 732, ], [ 'Method ReturnTypes\WrongMagicMethods::__unset() with return type void returns int but should not return anything.', - 735, + 737, ], [ 'Method ReturnTypes\WrongMagicMethods::__sleep() should return array but returns array.', - 740, + 742, ], [ 'Method ReturnTypes\WrongMagicMethods::__wakeup() with return type void returns int but should not return anything.', - 747, + 749, ], [ 'Method ReturnTypes\WrongMagicMethods::__set_state() should return object but returns array.', - 752, + 754, ], [ 'Method ReturnTypes\WrongMagicMethods::__clone() with return type void returns int but should not return anything.', - 757, + 759, ], [ 'Method ReturnTypes\ArrayFillKeysIssue::getIPs2() should return array> but returns array>.', - 815, + 817, ], [ 'Method ReturnTypes\AssertThisInstanceOf::doBar() should return $this(ReturnTypes\AssertThisInstanceOf) but returns ReturnTypes\AssertThisInstanceOf&ReturnTypes\FooInterface.', - 838, + 840, ], [ 'Method ReturnTypes\NestedArrayCheck::doFoo() should return array but returns array>.', - 858, + 860, ], [ 'Method ReturnTypes\NestedArrayCheck::doBar() should return array but returns array>.', - 873, + 875, ], [ 'Method ReturnTypes\Foo2::returnIntFromParent() should return int but returns string.', - 948, + 950, ], [ 'Method ReturnTypes\Foo2::returnIntFromParent() should return int but returns ReturnTypes\integer.', - 951, + 953, ], [ 'Method ReturnTypes\VariableOverwrittenInForeach::doFoo() should return int but returns int|string.', - 1009, + 1011, ], [ 'Method ReturnTypes\VariableOverwrittenInForeach::doBar() should return int but returns int|string.', - 1024, + 1026, ], [ 'Method ReturnTypes\ReturnStaticGeneric::instanceReturnsStatic() should return static(ReturnTypes\ReturnStaticGeneric) but returns ReturnTypes\ReturnStaticGeneric.', - 1064, + 1066, ], [ 'Method ReturnTypes\NeverReturn::doFoo() should never return but return statement found.', - 1238, + 1241, + ], + [ + 'Method ReturnTypes\NeverReturn::doBaz3() should never return but return statement found.', + 1254, ], ]); } @@ -366,24 +377,24 @@ public function testBug3997(): void { $this->analyse([__DIR__ . '/data/bug-3997.php'], [ [ - 'Method Bug3997\Foo::count() should return int but returns string.', - 12, + "Method Bug3997\Foo::count() should return int<0, max> but returns 'foo'.", + 13, ], [ - 'Method Bug3997\Bar::count() should return int but returns string.', - 22, + "Method Bug3997\Bar::count() should return int<0, max> but returns 'foo'.", + 24, ], [ 'Method Bug3997\Baz::count() should return int but returns string.', - 35, + 38, ], [ 'Method Bug3997\Lorem::count() should return int but returns string.', - 48, + 52, ], [ 'Method Bug3997\Dolor::count() should return int<0, max> but returns -1.', - 72, + 78, ], ]); } @@ -422,15 +433,15 @@ public function testBug4590(): void { $this->analyse([__DIR__ . '/data/bug-4590.php'], [ [ - 'Method Bug4590\Controller::test1() should return Bug4590\OkResponse> but returns Bug4590\OkResponse string)>.', + 'Method Bug4590\\Controller::test1() should return Bug4590\\OkResponse> but returns Bug4590\\OkResponse.', 39, ], [ - 'Method Bug4590\Controller::test2() should return Bug4590\OkResponse> but returns Bug4590\OkResponse.', + 'Method Bug4590\\Controller::test2() should return Bug4590\\OkResponse> but returns Bug4590\\OkResponse.', 47, ], [ - 'Method Bug4590\Controller::test3() should return Bug4590\OkResponse> but returns Bug4590\OkResponse.', + 'Method Bug4590\\Controller::test3() should return Bug4590\\OkResponse> but returns Bug4590\\OkResponse.', 55, ], ]); @@ -512,4 +523,184 @@ public function testBug3151(): void $this->analyse([__DIR__ . '/data/bug-3151.php'], []); } + public function testTemplateUnion(): void + { + $this->analyse([__DIR__ . '/data/return-template-union.php'], [ + [ + 'Method ReturnTemplateUnion\Foo::doFoo2() should return T of bool|float|int|string but returns (T of bool|float|int|string)|null.', + 25, + ], + ]); + } + + public function dataBug5218(): array + { + return [ + [ + true, + [ + [ + 'Method Bug5218\IA::getIterator() should return Traversable but returns ArrayIterator.', + 14, + ], + ], + ], + [ + false, + [], + ], + ]; + } + + /** + * @dataProvider dataBug5218 + * @param mixed[] $errors + */ + public function testBug5218(bool $checkExplicitMixed, array $errors): void + { + $this->checkExplicitMixed = $checkExplicitMixed; + $this->analyse([__DIR__ . '/data/bug-5218.php'], $errors); + } + + public function testBug5979(): void + { + $this->analyse([__DIR__ . '/data/bug-5979.php'], []); + } + + public function testBug4165(): void + { + if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->analyse([__DIR__ . '/data/bug-4165.php'], []); + } + + public function testBug6053(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6053.php'], []); + } + + public function testBug6438(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6438.php'], []); + } + + public function testBug6589(): void + { + $this->checkUnionTypes = false; + $this->analyse([__DIR__ . '/data/bug-6589.php'], [ + [ + 'Method Bug6589\HelloWorldTemplated::getField() should return TField of Bug6589\Field2 but returns Bug6589\Field.', + 17, + ], + [ + 'Method Bug6589\HelloWorldSimple::getField() should return Bug6589\Field2 but returns Bug6589\Field.', + 31, + ], + ]); + } + + public function testBug6418(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6418.php'], []); + } + + public function testBug6230(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6230.php'], []); + } + + public function testBug5860(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5860.php'], []); + } + + public function testBug6266(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6266.php'], []); + } + + public function testBug6023(): void + { + if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6023.php'], []); + } + + + public function testBug5065(): void + { + if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/bug-5065.php'], []); + } + + public function testBug5065ExplicitMixed(): void + { + if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5065.php'], [ + [ + 'Method Bug5065\Collection::emptyWorkaround2() should return Bug5065\Collection but returns Bug5065\Collection<(int|string), mixed>.', + 60, + ], + ]); + } + + public function testBug3400(): void + { + if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-3400.php'], []); + } + + public function testBug6353(): void + { + if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6353.php'], []); + } + + public function testBug6635Level9(): void + { + if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6635.php'], []); + } + + public function testBug6635Level8(): void + { + if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/bug-6635.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php b/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php new file mode 100644 index 0000000000..55eb7f75ec --- /dev/null +++ b/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php @@ -0,0 +1,100 @@ + + */ +class StaticMethodCallableRuleTest extends RuleTestCase +{ + + private int $phpVersion = PHP_VERSION_ID; + + protected function getRule(): Rule + { + $reflectionProvider = $this->createReflectionProvider(); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, true, false, true, false); + + return new StaticMethodCallableRule( + new StaticMethodCallCheck($reflectionProvider, $ruleLevelHelper, new ClassCaseSensitivityCheck($reflectionProvider, true), true, true), + new PhpVersion($this->phpVersion), + ); + } + + public function testNotSupportedOnOlderVersions(): void + { + if (PHP_VERSION_ID >= 80100) { + self::markTestSkipped('Test runs on PHP < 8.1.'); + } + if (!self::$useStaticReflectionProvider) { + self::markTestSkipped('Test requires static reflection.'); + } + + $this->analyse([__DIR__ . '/data/static-method-callable-not-supported.php'], [ + [ + 'First-class callables are supported only on PHP 8.1 and later.', + 10, + ], + ]); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/static-method-callable.php'], [ + [ + 'Call to static method StaticMethodCallable\Foo::doFoo() with incorrect case: dofoo', + 11, + ], + [ + 'Call to static method doFoo() on an unknown class StaticMethodCallable\Nonexistent.', + 12, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Call to an undefined static method StaticMethodCallable\Foo::nonexistent().', + 13, + ], + [ + 'Static call to instance method StaticMethodCallable\Foo::doBar().', + 14, + ], + [ + 'Call to private static method doBar() of class StaticMethodCallable\Bar.', + 15, + ], + [ + 'Cannot call abstract static method StaticMethodCallable\Bar::doBaz().', + 16, + ], + [ + 'Call to static method doFoo() on an unknown class StaticMethodCallable\Nonexistent.', + 21, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Cannot call static method doFoo() on int.', + 22, + ], + [ + 'Creating callable from a non-native static method StaticMethodCallable\Lorem::doBar().', + 47, + ], + [ + 'Creating callable from a non-native static method StaticMethodCallable\Ipsum::doBar().', + 66, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-3034.php b/tests/PHPStan/Rules/Methods/data/bug-3034.php index a83ebe47a5..f693787c09 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-3034.php +++ b/tests/PHPStan/Rules/Methods/data/bug-3034.php @@ -15,7 +15,7 @@ class HelloWorld implements \IteratorAggregate /** * @return \ArrayIterator */ - public function getIterator() + public function getIterator(): \Traversable { return new \ArrayIterator($this->list); } diff --git a/tests/PHPStan/Rules/Methods/data/bug-3400.php b/tests/PHPStan/Rules/Methods/data/bug-3400.php new file mode 100644 index 0000000000..b42066b9c9 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3400.php @@ -0,0 +1,34 @@ +values = $values; + } + + /** + * @param class-string $type + * + * @return Collection + * + * @template U of Immutable + */ + public static function ofType(string $type) : self + { + return new self(); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-3465.php b/tests/PHPStan/Rules/Methods/data/bug-3465.php new file mode 100644 index 0000000000..96cc78936a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3465.php @@ -0,0 +1,20 @@ +setValue(); +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-3478.php b/tests/PHPStan/Rules/Methods/data/bug-3478.php index 68b6993ec5..0bfd2f9209 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-3478.php +++ b/tests/PHPStan/Rules/Methods/data/bug-3478.php @@ -4,6 +4,7 @@ class ExtendedDocument extends \DOMDocument { + #[\ReturnTypeWillChange] public function saveHTML(\DOMNode $node = null) { return parent::saveHTML($node); diff --git a/tests/PHPStan/Rules/Methods/data/bug-3514.php b/tests/PHPStan/Rules/Methods/data/bug-3514.php new file mode 100644 index 0000000000..55a7156847 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3514.php @@ -0,0 +1,21 @@ +myRenamedMethod(); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-3530.php b/tests/PHPStan/Rules/Methods/data/bug-3530.php new file mode 100644 index 0000000000..d68eca7285 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3530.php @@ -0,0 +1,98 @@ +> */ + const SYNC_CLASSES = [ + 'a' => A::class, + 'b' => B::class, + 'c' => C::class, + 'd' => D::class, + 'e' => E::class, + 'f' => F::class, + 'g' => G::class, + 'h' => H::class, + 'i' => I::class, + 'j' => J::class, + ]; + + /** @param class-string $class */ + private function getRepository($class) : void + { + } + + public function getSyncEntity(string $type, string $syncId) : void + { + $class = self::SYNC_CLASSES[$type] ?? null; + if($class === null) { + return; + } + + $this->getRepository($class); + } + + public function getSyncEntity2(string $type, string $syncId) : void + { + $class = static::SYNC_CLASSES[$type] ?? null; + if($class === null) { + return; + } + + $this->getRepository($class); + } +} + +class HelloWorld2 +{ + const SYNC_CLASSES = [ + 'a' => A::class, + 'b' => B::class, + 'c' => C::class, + 'd' => D::class, + 'e' => E::class, + 'f' => F::class, + 'g' => G::class, + 'h' => H::class, + 'i' => I::class, + 'j' => J::class, + ]; + + /** @param class-string $class */ + private function getRepository($class) : void + { + } + + public function getSyncEntity(string $type, string $syncId) : void + { + $class = self::SYNC_CLASSES[$type] ?? null; + if($class === null) { + return; + } + + $this->getRepository($class); + } + + public function getSyncEntity2(string $type, string $syncId) : void + { + $class = static::SYNC_CLASSES[$type] ?? null; + if($class === null) { + return; + } + + $this->getRepository($class); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-3555.php b/tests/PHPStan/Rules/Methods/data/bug-3555.php new file mode 100644 index 0000000000..2a877f75b4 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-3555.php @@ -0,0 +1,31 @@ +run(100); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-3997.php b/tests/PHPStan/Rules/Methods/data/bug-3997.php index cbf3fda6ee..66278ec527 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-3997.php +++ b/tests/PHPStan/Rules/Methods/data/bug-3997.php @@ -7,6 +7,7 @@ class Foo implements Countable { + #[\ReturnTypeWillChange] public function count() { return 'foo'; @@ -17,6 +18,7 @@ public function count() class Bar implements Countable { + #[\ReturnTypeWillChange] public function count(): int { return 'foo'; @@ -30,6 +32,7 @@ class Baz implements Countable /** * @return int */ + #[\ReturnTypeWillChange] public function count(): int { return 'foo'; @@ -43,6 +46,7 @@ class Lorem implements Countable /** * @return int */ + #[\ReturnTypeWillChange] public function count() { return 'foo'; @@ -56,6 +60,7 @@ class Ipsum implements Countable /** * @return string */ + #[\ReturnTypeWillChange] public function count() { return 'foo'; @@ -67,6 +72,7 @@ class Dolor implements Countable { /** @return positive-int|0 */ + #[\ReturnTypeWillChange] public function count(): int { return -1; diff --git a/tests/PHPStan/Rules/Methods/data/bug-4084.php b/tests/PHPStan/Rules/Methods/data/bug-4084.php index 068ef3733b..f246e0e124 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-4084.php +++ b/tests/PHPStan/Rules/Methods/data/bug-4084.php @@ -8,10 +8,10 @@ class Handler implements \SessionUpdateTimestampHandlerInterface * @param string $sessionId * @param string $data */ - public function updateTimestamp($sessionId, $data) { return true; } + public function updateTimestamp($sessionId, $data): bool { return true; } /** * @param string $sessionId The session id */ - public function validateId($sessionId) { return true; } + public function validateId($sessionId): bool { return true; } } diff --git a/tests/PHPStan/Rules/Methods/data/bug-4165.php b/tests/PHPStan/Rules/Methods/data/bug-4165.php new file mode 100644 index 0000000000..fd9c4056ab --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4165.php @@ -0,0 +1,36 @@ +client = $client; + } + + /** + * @phpstan-return array<'int'|'stg'|'prd', int> + */ + public function __invoke(): array + { + $result = [ + 'int' => 3, + 'stg' => 4, + 'prd' => 5 + ]; + + $result[$this->client->env()] = 42; + + return $result; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4211.php b/tests/PHPStan/Rules/Methods/data/bug-4211.php new file mode 100644 index 0000000000..bb8c6270c1 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4211.php @@ -0,0 +1,28 @@ +format('j. n. Y'); + } +} + +class HelloWorld +{ + use HelloWorldTraitTest, HelloWorldTrait { + sayHello as hello; + } + + public function sayHello(DateTimeImmutable $date): void { + $this->hello($date); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4415.php b/tests/PHPStan/Rules/Methods/data/bug-4415.php index e440b56e79..247de77c3e 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-4415.php +++ b/tests/PHPStan/Rules/Methods/data/bug-4415.php @@ -2,6 +2,8 @@ namespace Bug4415Rule; +use function PHPStan\Testing\assertType; + /** * @template T * @extends \IteratorAggregate @@ -85,3 +87,10 @@ public function getName(): string return ''; } } + +function (CategoryCollection $c): void { + foreach ($c as $k => $v) { + assertType('mixed', $k); + assertType(Category::class, $v); + } +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-4516.php b/tests/PHPStan/Rules/Methods/data/bug-4516.php new file mode 100644 index 0000000000..df3b34a4c7 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4516.php @@ -0,0 +1,20 @@ +update_attributes([ + $name => $value, + ]); + } + + /** + * Updates as an array given attributes and saves the record. + * + * @param non-empty-array $attributes + * + * @return bool + */ + public function update_attributes($attributes) + { + return true; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4854.php b/tests/PHPStan/Rules/Methods/data/bug-4854.php new file mode 100644 index 0000000000..17a590c626 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4854.php @@ -0,0 +1,66 @@ + + * @extends \ArrayAccess + */ +interface DomainsAvailabilityInterface extends \IteratorAggregate, \ArrayAccess +{ + public const AVAILABLE = 1; + public const UNAVAILABLE = 2; + public const UNKNOWN = 3; +} + +abstract class AbstractDomainsAvailability implements DomainsAvailabilityInterface +{ + /** + * @var int[] + */ + protected array $domains; + + /** + * {@inheritdoc} + */ + public function getIterator(): \Traversable + { + return new \ArrayIterator($this->domains); + } + + /** + * {@inheritdoc} + */ + public function offsetSet($offset, $value): void + { + if ($offset === null) { + $this->domains[] = $value; + } else { + $this->domains[$offset] = $value; + } + } + + /** + * {@inheritdoc} + */ + public function offsetExists($offset): bool + { + return isset($this->domains[$offset]); + } + + /** + * {@inheritdoc} + */ + public function offsetUnset($offset): void + { + unset($this->domains[$offset]); + } + + /** + * {@inheritdoc} + */ + public function offsetGet($offset): int + { + return $this->domains[$offset]; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-4886.php b/tests/PHPStan/Rules/Methods/data/bug-4886.php new file mode 100644 index 0000000000..b29e43c9b0 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4886.php @@ -0,0 +1,31 @@ + + */ + private $source; + + /** + * @param callable(): iterable $callable + */ + public function __construct(callable $callable) + { + $this->source = $callable; + } + + /** + * @template NewTKey of array-key + * @template NewT + * + * @return self + */ + public static function empty(): self + { + return new self(static fn(): iterable => []); + } + + /** + * @template NewTKey of array-key + * @template NewT + * + * @return self + */ + public static function emptyWorkaround(): self + { + /** @var array $empty */ + $empty = []; + + return new self(static fn() => $empty); + } + + /** + * @template NewTKey of array-key + * @template NewT + * + * @return self + */ + public static function emptyWorkaround2(): self + { + /** @var Closure(): iterable */ + $func = static fn(): iterable => []; + + return new self($func); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5218.php b/tests/PHPStan/Rules/Methods/data/bug-5218.php new file mode 100644 index 0000000000..0c2022ce5c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5218.php @@ -0,0 +1,29 @@ + + */ +final class IA implements \IteratorAggregate +{ + /** @var array */ + private $data = []; + + public function getIterator() : \Traversable { + return new \ArrayIterator($this->data); + } +} + +class Foo +{ + + /** + * @return mixed + */ + public function doFoo() + { + return 1; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5253.php b/tests/PHPStan/Rules/Methods/data/bug-5253.php new file mode 100644 index 0000000000..d34fd47189 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5253.php @@ -0,0 +1,40 @@ +clientPool); + + do { + $client = next($this->clientPool); + + if (false === $client) { + $client = reset($this->clientPool); + + if (false === $client) { + throw new \Exception(); + } + } + + // Case when there is only one and the last one has been disabled + if ($last === $client && $client->isDisabled()) { + throw new \Exception(); + } + } while ($client->isDisabled()); + + return $client; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5258.php b/tests/PHPStan/Rules/Methods/data/bug-5258.php new file mode 100644 index 0000000000..27a751f859 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5258.php @@ -0,0 +1,23 @@ +method2($params); + + if (!empty($params['other_key'])) $this->method2($params); + } + + /** + * @param array{other_key:string} $params + **/ + public function method2(array$params): void + { + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5259.php b/tests/PHPStan/Rules/Methods/data/bug-5259.php new file mode 100644 index 0000000000..de650ccc0b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5259.php @@ -0,0 +1,15 @@ +date = $date instanceof \DateTimeImmutable ? $date : \DateTimeImmutable::createFromMutable($date); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5372.php b/tests/PHPStan/Rules/Methods/data/bug-5372.php new file mode 100644 index 0000000000..69a0a30a19 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5372.php @@ -0,0 +1,88 @@ += 7.4 + +namespace Bug5372; + +use function PHPStan\Testing\assertType; + +/** + * @template TKey of array-key + * @template T + */ +class Collection +{ + + /** @var array */ + private $values; + + /** + * @param array $values + */ + public function __construct(array $values) + { + $this->values = $values; + } + + /** + * @template V + * + * @param callable(T): V $callback + * + * @return self + */ + public function map(callable $callback): self { + return new self(array_map($callback, $this->values)); + } + + /** + * @template V of string + * + * @param callable(T): V $callback + * + * @return self + */ + public function map2(callable $callback): self { + return new self(array_map($callback, $this->values)); + } +} + +class Foo +{ + + /** @param Collection $list */ + function takesStrings(Collection $list): void { + echo serialize($list); + } + + /** @param class-string $classString */ + public function doFoo(string $classString) + { + $col = new Collection(['foo', 'bar']); + assertType('Bug5372\Collection', $col); + + $newCol = $col->map(static fn(string $var): string => $var . 'bar'); + assertType('Bug5372\Collection', $newCol); + $this->takesStrings($newCol); + + $newCol = $col->map(static fn(string $var): string => $classString); + assertType('Bug5372\Collection', $newCol); + $this->takesStrings($newCol); + + $newCol = $col->map2(static fn(string $var): string => $classString); + assertType('Bug5372\Collection', $newCol); + $this->takesStrings($newCol); + } + + /** @param literal-string $literalString */ + public function doBar(string $literalString) + { + $col = new Collection(['foo', 'bar']); + $newCol = $col->map(static fn(string $var): string => $literalString); + assertType('Bug5372\Collection', $newCol); + $this->takesStrings($newCol); + + $newCol = $col->map2(static fn(string $var): string => $literalString); + assertType('Bug5372\Collection', $newCol); // should be literal-string + $this->takesStrings($newCol); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5436.php b/tests/PHPStan/Rules/Methods/data/bug-5436.php new file mode 100644 index 0000000000..a79fa83492 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5436.php @@ -0,0 +1,12 @@ +setOptions(["timeout" => 1]); + + $request->getBody()->append(""); + + $client = new \http\Client(); + $client->enqueue($request)->send(); + + $response = $client->getResponse($request); +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-5536.php b/tests/PHPStan/Rules/Methods/data/bug-5536.php new file mode 100644 index 0000000000..18c305a9e3 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5536.php @@ -0,0 +1,47 @@ + + */ +class Model +{ + /** + * @param array $args + */ + public function __call(string $method, array $args) + { + return $this->$method; + } + + /** + * @param array $args + */ + public static function __callStatic(string $method, array $args) + { + return (new static)->$method(...$args); + } +} + +/** + * @template TModel of Model + */ +class Builder +{ + /** + * @return array + */ + public function all(): array + { + return []; + } +} + +class User extends Model {} + +function (): void { + User::all(); + $user = new User(); + $user->all(); +}; diff --git a/tests/PHPStan/Rules/Methods/data/bug-5562.php b/tests/PHPStan/Rules/Methods/data/bug-5562.php new file mode 100644 index 0000000000..3bcf4b1d4e --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5562.php @@ -0,0 +1,34 @@ +bar($test); + assertType('T of int|string (method Bug5562\Foo::foo(), argument)', $bar); + + return $bar; + } + + /** + * @template T of int|string + * @param T $test + * @return T + */ + public function bar($test) + { + return $test; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5591.php b/tests/PHPStan/Rules/Methods/data/bug-5591.php new file mode 100644 index 0000000000..b8510d13ee --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5591.php @@ -0,0 +1,46 @@ + The fully-qualified (::class) class name of the entity being managed. */ + protected $entityClass; + + /** @param TEntity|null $record */ + public function outerMethod($record = null): void + { + $record = $this->innerMethod($record); + } + + /** + * @param TEntity|null $record + * + * @return TEntity + */ + public function innerMethod($record = null): object + { + $class = $this->entityClass; + return new $class(); + } +} + +/** + * @template TEntity as EntityA|EntityB + * @extends TestClass + */ +class TestClass2 extends TestClass +{ + public function outerMethod($record = null): void + { + $record = $this->innerMethod($record); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5749.php b/tests/PHPStan/Rules/Methods/data/bug-5749.php new file mode 100644 index 0000000000..4ca314c953 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5749.php @@ -0,0 +1,50 @@ +|null', $type); + + if ($type) { + assertType('non-empty-array', $type); + $typeSql = ' AND type IN ' . self::dbarray_int($type) . ' '; + } else { + assertType('0|array{}|null', $type); + $typeSql = ''; + } + + // ... + + return $typeSql; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5757.php b/tests/PHPStan/Rules/Methods/data/bug-5757.php new file mode 100644 index 0000000000..64fa9b4de3 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5757.php @@ -0,0 +1,29 @@ + $iterable + * @phpstan-return iterable> + */ + public static function chunk(iterable $iterable, int $chunkSize): iterable + { + return []; + } +} + +class Foo +{ + + public function doFoo() + { + assertType('iterable>', Helper::chunk([1], 3)); + assertType('iterable>', Helper::chunk([], 3)); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5860.php b/tests/PHPStan/Rules/Methods/data/bug-5860.php new file mode 100644 index 0000000000..f41cab6c93 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5860.php @@ -0,0 +1,20 @@ += 8.0 + +namespace Bug5868; + +class HelloWorld +{ + public function nullable1(): ?self + { + // OK + $tmp = $this->nullable1()?->nullable1()?->nullable2(); + $tmp = $this->nullable1()?->nullable3()->nullable2()?->nullable3()->nullable1(); + + // Error + $tmp = $this->nullable1()->nullable1()?->nullable2(); + $tmp = $this->nullable1()?->nullable1()->nullable2(); + $tmp = $this->nullable1()?->nullable3()->nullable2()->nullable3()->nullable1(); + + return $this->nullable1()?->nullable3(); + } + + public function nullable2(): ?self + { + return $this; + } + + public function nullable3(): self + { + return $this; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5869.php b/tests/PHPStan/Rules/Methods/data/bug-5869.php new file mode 100644 index 0000000000..43fd5607c2 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5869.php @@ -0,0 +1,32 @@ +sayHello($class); + } + + /** + * @param T $class + */ + public function sayHello(BaseInterface $class): void + { + echo 'Hello', PHP_EOL; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5893.php b/tests/PHPStan/Rules/Methods/data/bug-5893.php new file mode 100644 index 0000000000..f1666d75fe --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5893.php @@ -0,0 +1,31 @@ + + */ + public static function getClass($object) + { + return get_class($object); + } +} + +/** + * @phpstan-template T of object + */ +class Foo { + /** @phpstan-param T $object */ + public function foo(object $object): string { + if (method_exists($object, '__toString') && null !== $object->__toString()) { + return $object->__toString(); + } + + return Test::getClass($object); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-5898.php b/tests/PHPStan/Rules/Methods/data/bug-5898.php new file mode 100644 index 0000000000..cbf5e3cb47 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-5898.php @@ -0,0 +1,25 @@ + + */ + public function dataProviderForTestValidCommands(): array + { + $data = [ + // left out some commands here for simplicity ... + // [...] + [ + 'migrations:execute', + SplQueue::class, + ], + ]; + + // this is only available with DBAL 2.x + if (class_exists(ImportCommand::class)) { + $data[] = [ + 'dbal:import', + ImportCommand::class, + ]; + } + + return $data; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6023.php b/tests/PHPStan/Rules/Methods/data/bug-6023.php new file mode 100644 index 0000000000..d2eb85eac2 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6023.php @@ -0,0 +1,22 @@ += 7.4 + +namespace Bug6023; + +class Grouper +{ + /** + * @param array{commissions: array, leftovers: array} $groups + * @return array{commissions: array, leftovers: array} + */ + public function groupByType(array $groups, Clearable $clearable): array + { + $group = $clearable->type === 'foo' ? 'commissions' : 'leftovers'; + $groups[$group][] = $clearable; + return $groups; + } +} + +class Clearable +{ + public string $type = 'foo'; +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6053.php b/tests/PHPStan/Rules/Methods/data/bug-6053.php new file mode 100644 index 0000000000..0a2de15614 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6053.php @@ -0,0 +1,29 @@ + + */ + public function processItems($items): ?array + { + if ($items === null || !is_array($items)) { + return null; + } + + if ($items === []) { + return []; + } + + $result = []; + foreach ($items as $item) { + $result[] = 'something'; + } + + return $result; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6055.php b/tests/PHPStan/Rules/Methods/data/bug-6055.php new file mode 100644 index 0000000000..027a9486ec --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6055.php @@ -0,0 +1,23 @@ +check(null); + + $this->check(array_merge( + ['key1' => true], + ['key2' => 'value'] + )); + } + + /** + * @param ?array $items + */ + private function check(?array $items): void + { + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6081.php b/tests/PHPStan/Rules/Methods/data/bug-6081.php new file mode 100644 index 0000000000..aff80084e1 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6081.php @@ -0,0 +1,19 @@ +something($array); + if(count($array) !== 0){ + $this->something($array); + } + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6118.php b/tests/PHPStan/Rules/Methods/data/bug-6118.php new file mode 100644 index 0000000000..8b5d81a8fc --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6118.php @@ -0,0 +1,68 @@ += 8.0 + +namespace Bug6118; + + +/** + * @template-covariant T of mixed + */ +class Element { + /** @var T */ + public mixed $value; + + /** + * @param Element $element + */ + function getValue(Element $element) : void { + } + + /** + * @param Element $element + */ + function takesValue(Element $element) : void { + $this->getValue($element); + } + + + /** + * @param Element $element + */ + function getValue2(Element $element) : void { + } + + /** + * @param Element $element + */ + function takesValue2(Element $element) : void { + getValue2($element); + } +} + +/** + * @template-covariant T of string|int|array + */ +interface Example { + /** + * @return T|null + */ + public function normalize(): string|int|array|null; +} + +/** + * @implements Example> + */ +class Foo implements Example { + public function normalize(): int + { + return 0; + } + /** + * @param Example> $example + */ + function foo(Example $example): void { + } + + function bar(): void { + $this->foo(new Foo()); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6230.php b/tests/PHPStan/Rules/Methods/data/bug-6230.php new file mode 100644 index 0000000000..64e24a052f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6230.php @@ -0,0 +1,43 @@ + $it + * @return ?iterable + */ + function test($it): ?iterable + { + return $it; + } + +} + +/** + * @template T + */ +class Example +{ + /** + * @var ?iterable + */ + private $input; + + + /** + * @param iterable $input + */ + public function __construct(iterable $input) + { + $this->input = $input; + } + + /** @return ?iterable */ + public function get(): ?iterable + { + return $this->input; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6236.php b/tests/PHPStan/Rules/Methods/data/bug-6236.php new file mode 100644 index 0000000000..167be4353a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6236.php @@ -0,0 +1,27 @@ + $t + */ + public static function sayHello(\Traversable $t): void + { + } + + /** + * @param \SplObjectStorage<\DateTime, \DateTime> $foo + */ + public function doFoo($foo) + { + $this->sayHello(new \ArrayIterator([new \DateTime()])); + + $this->sayHello(new \ArrayIterator(['a' => new \DateTime()])); + + $this->sayHello($foo); + } +} + diff --git a/tests/PHPStan/Rules/Methods/data/bug-6249.php b/tests/PHPStan/Rules/Methods/data/bug-6249.php new file mode 100644 index 0000000000..d25561ebc3 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6249.php @@ -0,0 +1,136 @@ += 7.4 + +namespace Bug6249N1; + +interface EntityInterface +{ + public function getId(): string; +} + +use Countable; +use IteratorAggregate; + +/** + * @template TKey of array-key + * @template T + * @template-extends IteratorAggregate + */ +interface CollectionInterface extends Countable, IteratorAggregate +{ +} + +namespace Bug6249N2; + +use ArrayIterator; +use InvalidArgumentException; +use IteratorIterator; +use Bug6249N1\EntityInterface; +use Traversable; +/** + * @extends \IteratorIterator> + */ +final class Eii extends IteratorIterator +{ + /** + * @param iterable $iterable + */ + public function __construct(iterable $iterable) + { + parent::__construct($iterable instanceof Traversable ? $iterable : new ArrayIterator($iterable)); + } + + /** + * @return EntityInterface + */ + public function current() + { + $current = parent::current(); + + if (!$current instanceof EntityInterface) { + throw new InvalidArgumentException(sprintf('Item "%s" must be an instance of "%s".', gettype($current), EntityInterface::class)); + } + + return $current; + } + + /** + * return ?string + */ + public function key() + { + if ($this->valid()) { + /** @var EntityInterface $current */ + $current = $this->current(); + + return $current->getId(); + } + + return null; + } +} + +namespace Bug6249N3; + +use ArrayIterator; +use Countable; +use Bug6249N1\CollectionInterface; +use Traversable; + +/** + * @template TKey of array-key + * @template T + * @implements CollectionInterface + */ +final class Cw implements CollectionInterface +{ + /** + * @var iterable + */ + private iterable $iterable; + + /** + * @param iterable $iterable + */ + private function __construct(iterable $iterable) + { + $this->iterable = $iterable; + } + + /** + * @param iterable $iterable + * + * @return self + */ + public static function fromIterable(iterable $iterable): self + { + return new self($iterable); + } + + public function count(): int + { + if (is_array($this->iterable) || $this->iterable instanceof Countable) { + return count($this->iterable); + } + + return count(iterator_to_array($this->iterable, false)); + } + + public function getIterator(): Traversable + { + if (is_array($this->iterable)) { + return new ArrayIterator($this->iterable); + } + + return $this->iterable; + } +} + +class Foo +{ + + public function doFoo() + { + \Bug6249N3\Cw::fromIterable(new \Bug6249N2\Eii([])); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6264.php b/tests/PHPStan/Rules/Methods/data/bug-6264.php new file mode 100644 index 0000000000..f8f10ca26a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6264.php @@ -0,0 +1,42 @@ +doFooImpl(); + } +} + +class FooBar extends Bar +{ + use SpecificFoo; +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6266.php b/tests/PHPStan/Rules/Methods/data/bug-6266.php new file mode 100644 index 0000000000..058e18eec3 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6266.php @@ -0,0 +1,27 @@ +myNumber(); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6353.php b/tests/PHPStan/Rules/Methods/data/bug-6353.php new file mode 100644 index 0000000000..f9542a4c15 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6353.php @@ -0,0 +1,69 @@ + + */ + function some($t): Option { + /** @implements Option */ + return new class($t) implements Option { + /** + * @param T $t + */ + public function __construct(private $t) { + } + + /** + * @return T + */ + public function get() { + return $this->t; + } + }; + } + + /** + * @return Option + */ + function none(): Option { + /** @implements Option */ + return new class() implements Option { + + /** + * @return never + */ + public function get() { + throw new \Exception(); + } + }; + } + /** + * @template T + * @param callable():T $fn + * @return Option + */ + function fromCallbackThatCanThrow(callable $fn) { + try { + $a = $this->some($fn()); + } catch (\Throwable $failure) { + $a = $this->none(); + } + return $a; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6418.php b/tests/PHPStan/Rules/Methods/data/bug-6418.php new file mode 100644 index 0000000000..d1fb02a4fe --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6418.php @@ -0,0 +1,19 @@ += 7.4 + +namespace Bug6423; + +class Foo +{ + + + /** + * @param null|list $foos + */ + function doFoo(?array $foos = null): void {} + + /** + * @return list + */ + function doBar(): array + { + return [ + 'hello', + 'world', + ]; + } + + function doBaz() + { + $this->doFoo([ + 'foo', + 'bar', + ...$this->doBar(), + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6438.php b/tests/PHPStan/Rules/Methods/data/bug-6438.php new file mode 100644 index 0000000000..826725d665 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6438.php @@ -0,0 +1,44 @@ + $value, 'description' => $description]; + } + + /** + * @phpstan-return array{value: int, description: string}|null + */ + public function testInteger() + { + return $this->getValueDescription(5, 'Description'); + } + + /** + * @phpstan-return array{value: bool, description: string}|null + */ + public function testBooleanTrue() + { + return $this->getValueDescription(true, 'Description'); + } + + /** + * @phpstan-return array{value: bool, description: string}|null + */ + public function testBooleanFalse() + { + return $this->getValueDescription(false, 'Description'); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6464.php b/tests/PHPStan/Rules/Methods/data/bug-6464.php new file mode 100644 index 0000000000..0cc01b2790 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6464.php @@ -0,0 +1,20 @@ + $g */ + public function foo(\Generator $g): void; +} + +class Bar +{ + + function test(Foo $foo): void { + $foo->foo((function(string $str) { + yield $str; + })('hello')); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-6472.php b/tests/PHPStan/Rules/Methods/data/bug-6472.php new file mode 100644 index 0000000000..db2ca978a7 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6472.php @@ -0,0 +1,29 @@ += 8.1 + +namespace CallMethodInEnum; + +enum Foo +{ + + public function doFoo() + { + $this->doFoo(); + $this->doNonexistent(); + } + +} + +trait FooTrait +{ + + public function doFoo() + { + $this->doFoo(); + $this->doNonexistent(); + } + +} + +enum Bar +{ + + use FooTrait; + +} diff --git a/tests/PHPStan/Rules/Methods/data/call-methods.php b/tests/PHPStan/Rules/Methods/data/call-methods.php index 3d5669648c..081b913ea1 100644 --- a/tests/PHPStan/Rules/Methods/data/call-methods.php +++ b/tests/PHPStan/Rules/Methods/data/call-methods.php @@ -1678,3 +1678,127 @@ public function openStatically(): void } } + +class HelloWorld +{ + /** + * @param \DateTime|\DateTimeImmutable|int $date + */ + public function sayHello($date): void + { + } + + /** + * @param \DateTimeInterface|int $d + */ + public function foo($d): void + { + $this->sayHello($d); + } +} + +class HelloWorld2 +{ + /** + * @param \DateTime|\DateTimeImmutable $date + */ + public function sayHello($date): void + { + } + + /** + * @param \DateTimeInterface $d + */ + public function foo($d): void + { + $this->sayHello($d); + } +} + +class HelloWorld3 +{ + /** + * @param array<\DateTime|\DateTimeImmutable>|int $date + */ + public function sayHello($date): void + { + } + + /** + * @param \DateTimeInterface $d + */ + public function foo($d): void + { + $this->sayHello($d); + } +} + +class InvalidReturnTypeUsingArrayTemplateTypeBound +{ + + /** + * @template T of array + * @param T $a + * @return T + */ + function bar(array $a): array + { + return $a; + } + + public function doBar() + { + $this->bar(range(1, 3)); + } + +} + +class KeyOfParam +{ + public const JFK = 'jfk'; + public const LGA = 'lga'; + + private const ALL = [ + self::JFK => 'John F. Kennedy Airport', + self::LGA => 'La Guardia Airport', + ]; + + /** + * @param key-of $code + */ + public function foo(string $code): void + { + } + + public function test(): void + { + $this->foo(KeyOfParam::JFK); + $this->foo('jfk'); + $this->foo('sfo'); + } +} + +class ValueOfParam +{ + public const JFK = 'jfk'; + public const LGA = 'lga'; + + public const ALL = [ + self::JFK => 'John F. Kennedy Airport', + self::LGA => 'La Guardia Airport', + ]; + + /** + * @param value-of $code + */ + public function foo(string $code): void + { + } + + public function test(): void + { + $this->foo(ValueOfParam::ALL[ValueOfParam::JFK]); + $this->foo('John F. Kennedy Airport'); + $this->foo('Newark Liberty International'); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/call-private-method-static.php b/tests/PHPStan/Rules/Methods/data/call-private-method-static.php new file mode 100644 index 0000000000..f273d48d73 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/call-private-method-static.php @@ -0,0 +1,37 @@ += 8.0 namespace CheckExplicitMixedMethodCall; @@ -67,3 +67,89 @@ public function doLorem($t): void } } + +class TemplateMixed +{ + + /** + * @template T + * @param T $t + */ + public function doFoo($t): void + { + $this->doBar($t); + } + + /** + * @param mixed $mixed + */ + public function doBar($mixed): void + { + $this->doFoo($mixed); + } + +} + +class CallableMixed +{ + + /** + * @param callable(mixed): void $cb + */ + public function doFoo(callable $cb): void + { + + } + + /** + * @param callable(int): void $cb + */ + public function doBar(callable $cb): void + { + + } + + /** + * @param callable(): mixed $cb + */ + public function doFoo2(callable $cb): void + { + + } + + /** + * @param callable(): int $cb + */ + public function doBar2(callable $cb): void + { + + } + + public function doLorem(int $i, mixed $m): void + { + $acceptsInt = function (int $i): void { + + }; + $this->doFoo($acceptsInt); + $this->doBar($acceptsInt); + + $acceptsMixed = function (mixed $m): void { + + }; + $this->doFoo($acceptsMixed); + $this->doBar($acceptsMixed); + + $returnsInt = function () use ($i): int { + return $i; + }; + $this->doFoo2($returnsInt); + $this->doBar2($returnsInt); + + $returnsMixed = function () use ($m): mixed { + return $m; + }; + $this->doFoo2($returnsMixed); + $this->doBar2($returnsMixed); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/closure-bind.php b/tests/PHPStan/Rules/Methods/data/closure-bind.php index e4dec44d16..f6aa9e05b7 100644 --- a/tests/PHPStan/Rules/Methods/data/closure-bind.php +++ b/tests/PHPStan/Rules/Methods/data/closure-bind.php @@ -33,6 +33,11 @@ public function fooMethod(): Foo $foo->nonexistentMethod(); }, null, new Foo()); + \Closure::bind(function (Foo $foo) { + $foo->privateMethod(); + $foo->nonexistentMethod(); + }, null, get_class(new Foo())); + \Closure::bind(function () { // $this is Foo $this->privateMethod(); diff --git a/tests/PHPStan/Rules/Methods/data/countable-bug.php b/tests/PHPStan/Rules/Methods/data/countable-bug.php new file mode 100644 index 0000000000..9a3c4aa6e3 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/countable-bug.php @@ -0,0 +1,17 @@ + */ +interface IteratorChild2 extends \Iterator +{ + + /** @return int */ + #[\ReturnTypeWillChange] + public function key(); + + /** @return int */ + #[\ReturnTypeWillChange] + public function current(); + +} + +class Foo +{ + + public function doFoo(IteratorChild $c) + { + foreach ($c as $k => $v) { + assertType('int', $k); + assertType('int', $v); + } + } + + public function doFoo2(IteratorChild2 $c) + { + foreach ($c as $k => $v) { + assertType('mixed', $k); + assertType('mixed', $v); + } + } + +} + +interface IteratorChild3 extends \Iterator +{ + +} + +class IteratorChildTest +{ + + public function doFoo(IteratorChild3 $c) + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/first-class-callable-method-without-side-effect.php b/tests/PHPStan/Rules/Methods/data/first-class-callable-method-without-side-effect.php new file mode 100644 index 0000000000..78489a0c19 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/first-class-callable-method-without-side-effect.php @@ -0,0 +1,42 @@ += 8.1 + +namespace FirstClassCallableMethodWithoutSideEffect; + +class Foo +{ + + public function doFoo(): void + { + $f = $this->doFoo(...); + + $this->doFoo(...); + } + +} + +class Bar +{ + + function doFoo(): never + { + throw new \Exception(); + } + + /** + * @throws \Exception + */ + function doBar() + { + throw new \Exception(); + } + + function doBaz(): void + { + $f = $this->doFoo(...); + $this->doFoo(...); + + $g = $this->doBar(...); + $this->doBar(...); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/first-class-callable-static-method-without-side-effect.php b/tests/PHPStan/Rules/Methods/data/first-class-callable-static-method-without-side-effect.php new file mode 100644 index 0000000000..b387cd8be0 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/first-class-callable-static-method-without-side-effect.php @@ -0,0 +1,42 @@ += 8.1 + +namespace FirstClassCallableStaticMethodWithoutSideEffect; + +class Foo +{ + + public static function doFoo(): void + { + $f = self::doFoo(...); + + self::doFoo(...); + } + +} + +class Bar +{ + + static function doFoo(): never + { + throw new \Exception(); + } + + /** + * @throws \Exception + */ + static function doBar() + { + throw new \Exception(); + } + + function doBaz(): void + { + $f = self::doFoo(...); + self::doFoo(...); + + $g = self::doBar(...); + self::doBar(...); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/first-class-method-callable.php b/tests/PHPStan/Rules/Methods/data/first-class-method-callable.php new file mode 100644 index 0000000000..c969b924c2 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/first-class-method-callable.php @@ -0,0 +1,13 @@ += 8.1 + +namespace FirstClassMethodCallable; + +class Foo +{ + + public function doFoo(int $i): void + { + $this->doFoo(...); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/first-class-static-method-callable.php b/tests/PHPStan/Rules/Methods/data/first-class-static-method-callable.php new file mode 100644 index 0000000000..755e9be311 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/first-class-static-method-callable.php @@ -0,0 +1,13 @@ += 8.1 + +namespace FirstClassStaticMethodCallable; + +class Foo +{ + + public static function doFoo(int $i): void + { + self::doFoo(...); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/generics-empty-array.php b/tests/PHPStan/Rules/Methods/data/generics-empty-array.php new file mode 100644 index 0000000000..3eec6a01da --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/generics-empty-array.php @@ -0,0 +1,25 @@ + $a + * @return array{TKey, T} + */ + public function doFoo(array $a = []): array + { + + } + + public function doBar() + { + $this->doFoo(); + $this->doFoo([]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/generics-infer-collection.php b/tests/PHPStan/Rules/Methods/data/generics-infer-collection.php new file mode 100644 index 0000000000..8c17507d70 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/generics-infer-collection.php @@ -0,0 +1,76 @@ + $items + */ + public function __construct(array $items = []) + { + + } + +} + +/** + * @template TKey of array-key + * @template T + */ +class ArrayCollection2 +{ + + public function __construct(array $items = []) + { + + } + +} + +class Foo +{ + + public function doFoo() + { + $this->doBar(new ArrayCollection()); + $this->doBar(new ArrayCollection([])); + $this->doBar(new ArrayCollection(['foo', 'bar'])); + } + + /** + * @param ArrayCollection $c + * @return void + */ + public function doBar(ArrayCollection $c) + { + + } + +} + +class Bar +{ + + public function doFoo() + { + $this->doBar(new ArrayCollection2()); + $this->doBar(new ArrayCollection2([])); + $this->doBar(new ArrayCollection2(['foo', 'bar'])); + } + + /** + * @param ArrayCollection2 $c + * @return void + */ + public function doBar(ArrayCollection2 $c) + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/infer-array-key.php b/tests/PHPStan/Rules/Methods/data/infer-array-key.php index 7107f700c2..86ab98a1b5 100644 --- a/tests/PHPStan/Rules/Methods/data/infer-array-key.php +++ b/tests/PHPStan/Rules/Methods/data/infer-array-key.php @@ -13,6 +13,7 @@ class Foo implements \IteratorAggregate /** @var \stdClass[] */ private $items; + #[\ReturnTypeWillChange] public function getIterator() { $it = new \ArrayIterator($this->items); @@ -32,6 +33,7 @@ class Bar implements \IteratorAggregate /** @var array */ private $items; + #[\ReturnTypeWillChange] public function getIterator() { $it = new \ArrayIterator($this->items); @@ -51,6 +53,7 @@ class Baz implements \IteratorAggregate /** @var array */ private $items; + #[\ReturnTypeWillChange] public function getIterator() { $it = new \ArrayIterator($this->items); @@ -70,6 +73,7 @@ class Lorem implements \IteratorAggregate /** @var array<\stdClass> */ private $items; + #[\ReturnTypeWillChange] public function getIterator() { $it = new \ArrayIterator($this->items); @@ -89,6 +93,7 @@ class Ipsum implements \IteratorAggregate /** @var array */ private $items; + #[\ReturnTypeWillChange] public function getIterator() { $it = new \ArrayIterator($this->items); diff --git a/tests/PHPStan/Rules/Methods/data/intersection-types.php b/tests/PHPStan/Rules/Methods/data/intersection-types.php new file mode 100644 index 0000000000..6a3987d9b6 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/intersection-types.php @@ -0,0 +1,43 @@ += 8.1 + +namespace MethodIntersectionTypes; + +interface Foo +{ + +} + +interface Bar +{ + +} + +class Lorem +{ + +} + +class Ipsum +{ + +} + +class Foo +{ + + public function doFoo(Foo&Bar $a): Foo&Bar + { + + } + + public function doBar(Lorem&Ipsum $a): Lorem&Ipsum + { + + } + + public function doBaz(int&mixed $a): int&mixed + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/literal-string.php b/tests/PHPStan/Rules/Methods/data/literal-string.php new file mode 100644 index 0000000000..e919270eb0 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/literal-string.php @@ -0,0 +1,70 @@ +requireLiteralString($string); + $this->requireLiteralString($literalString); + $this->requireLiteralString('foo'); + $this->requireLiteralString($int); + $this->requireLiteralString(1); + + $mixed = doFoo(); + $this->requireLiteralString($mixed); + } + + /** + * @param literal-string $s + */ + public function requireLiteralString(string $s): void + { + + } + + /** + * @param array $a + */ + public function requireArrayOfLiteralStrings(array $a): void + { + + } + + /** + * @param mixed $mixed + * @param array $arrayOfStrings + * @param array $arrayOfLiteralStrings + * @param array $arrayOfMixed + */ + public function doBar( + $mixed, + array $arrayOfStrings, + array $arrayOfLiteralStrings, + array $arrayOfMixed + ): void + { + $this->requireArrayOfLiteralStrings($mixed); + $this->requireArrayOfLiteralStrings($arrayOfStrings); + $this->requireArrayOfLiteralStrings($arrayOfLiteralStrings); + $this->requireArrayOfLiteralStrings($arrayOfMixed); + } + + public function doGet(): void + { + $this->requireLiteralString($_GET); + $this->requireLiteralString($_GET['x']); + $this->requireLiteralString($_GET['x']['y']); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/memcache-pool-get.php b/tests/PHPStan/Rules/Methods/data/memcache-pool-get.php new file mode 100644 index 0000000000..711560b787 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/memcache-pool-get.php @@ -0,0 +1,13 @@ += 8.1 + +namespace MethodCallableNotSupported; + +class Foo +{ + + public function doFoo(): void + { + $this->doFoo(...); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/method-callable.php b/tests/PHPStan/Rules/Methods/data/method-callable.php new file mode 100644 index 0000000000..b86bca74e0 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/method-callable.php @@ -0,0 +1,88 @@ += 8.1 + +namespace MethodCallable; + +class Foo +{ + + public function doFoo(int $i): void + { + $this->doFoo(...); + $this->dofoo(...); + $this->doNonexistent(...); + $i->doFoo(...); + } + + public function doBar(Bar $bar): void + { + $bar->doBar(...); + } + + public function doBaz(Nonexistent $n): void + { + $n->doFoo(...); + } + +} + +class Bar +{ + + private function doBar() + { + + } + +} + +class ParentClass +{ + + private function doFoo() + { + + } + +} + +class ChildClass extends ParentClass +{ + + public function doBar() + { + $this->doFoo(...); + } + +} + +/** + * @method void doBar() + */ +class Lorem +{ + + public function doFoo() + { + $this->doBar(...); + } + + public function __call($name, $arguments) + { + + } + + +} + +/** + * @method void doBar() + */ +class Ipsum +{ + + public function doFoo() + { + $this->doBar(...); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/missing-method-impl-enum.php b/tests/PHPStan/Rules/Methods/data/missing-method-impl-enum.php new file mode 100644 index 0000000000..28f00b429e --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/missing-method-impl-enum.php @@ -0,0 +1,24 @@ += 8.1 + +namespace MethodNewInInitializers; + +class Foo +{ + + /** + * @param int $i + */ + public function doFoo($i = new \stdClass(), object $o = new \stdClass()) + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/non-empty-string-verbosity.php b/tests/PHPStan/Rules/Methods/data/non-empty-string-verbosity.php new file mode 100644 index 0000000000..3437ea767e --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/non-empty-string-verbosity.php @@ -0,0 +1,21 @@ +doBar($s); + } + + public function doBar(int $i): void + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/readonly-property-passed-by-reference.php b/tests/PHPStan/Rules/Methods/data/readonly-property-passed-by-reference.php new file mode 100644 index 0000000000..d53cf4c345 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/readonly-property-passed-by-reference.php @@ -0,0 +1,24 @@ += 8.1 + +namespace ReadonlyPropertyPassedByRef; + +class Foo +{ + + private int $foo; + + private readonly int $bar; + + public function doFoo() + { + $this->doBar($this->foo); + $this->doBar($this->bar); + $this->doBar(param: $this->bar); + } + + public function doBar(&$param): void + { + + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/rector-do-while-var-issue.php b/tests/PHPStan/Rules/Methods/data/rector-do-while-var-issue.php new file mode 100644 index 0000000000..e30eb8336d --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/rector-do-while-var-issue.php @@ -0,0 +1,39 @@ +processCharacterClass($cls); + } else { + $cls = 'foo'; + } + } while (doFoo()); + } + + public function doFoo2(string $cls): void + { + do { + if (doBar()) { + [$cls] = $this->processCharacterClass($cls); + } else { + $cls = 'foo'; + } + } while (doFoo()); + } + + /** + * @return int[]|string[] + */ + private function processCharacterClass(string $cls): array + { + return []; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/return-template-union.php b/tests/PHPStan/Rules/Methods/data/return-template-union.php new file mode 100644 index 0000000000..a154bfd03d --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/return-template-union.php @@ -0,0 +1,38 @@ + */ @@ -1206,6 +1208,7 @@ public function add($value, $key = null): void /** * @return CollectionKey|null */ + #[\ReturnTypeWillChange] public function key() { return key($this->data); @@ -1238,4 +1241,18 @@ public function doFoo(): void return; } + /** + * @return never + */ + public function doBaz3(): string + { + try { + throw new \Exception('try'); + } catch (\Exception $e) { + throw new \Exception('catch'); + } finally { + return 'finally'; + } + } + } diff --git a/tests/PHPStan/Rules/Methods/data/static-method-callable-not-supported.php b/tests/PHPStan/Rules/Methods/data/static-method-callable-not-supported.php new file mode 100644 index 0000000000..0aaec79db5 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/static-method-callable-not-supported.php @@ -0,0 +1,13 @@ += 8.1 + +namespace StaticMethodCallableNotSupported; + +class Foo +{ + + public static function doFoo(): void + { + self::doFoo(...); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/static-method-callable.php b/tests/PHPStan/Rules/Methods/data/static-method-callable.php new file mode 100644 index 0000000000..d8256c1c4b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/static-method-callable.php @@ -0,0 +1,69 @@ += 8.1 + +namespace StaticMethodCallable; + +class Foo +{ + + public static function doFoo() + { + self::doFoo(...); + self::dofoo(...); + Nonexistent::doFoo(...); + self::nonexistent(...); + self::doBar(...); + Bar::doBar(...); + Bar::doBaz(...); + } + + public function doBar(Nonexistent $n, int $i) + { + $n::doFoo(...); + $i::doFoo(...); + } + +} + +abstract class Bar +{ + + private static function doBar() + { + + } + + abstract public static function doBaz(); + +} + +/** + * @method static void doBar() + */ +class Lorem +{ + + public function doFoo() + { + self::doBar(...); + } + + public function __call($name, $arguments) + { + + } + + +} + +/** + * @method static void doBar() + */ +class Ipsum +{ + + public function doFoo() + { + self::doBar(...); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/tentative-return-types.php b/tests/PHPStan/Rules/Methods/data/tentative-return-types.php new file mode 100644 index 0000000000..d42f319ae0 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/tentative-return-types.php @@ -0,0 +1,45 @@ + - */ -class MissingClosureNativeReturnTypehintRuleTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new MissingClosureNativeReturnTypehintRule(true); - } - - public function testRule(): void - { - $this->analyse([__DIR__ . '/data/missing-closure-native-return-typehint.php'], [ - [ - 'Anonymous function should have native return typehint "void".', - 10, - ], - [ - 'Anonymous function should have native return typehint "void".', - 13, - ], - [ - 'Anonymous function should have native return typehint "Generator".', - 16, - ], - [ - 'Mixing returning values with empty return statements - return null should be used here.', - 25, - ], - [ - 'Anonymous function should have native return typehint "?int".', - 23, - ], - [ - 'Anonymous function should have native return typehint "?int".', - 33, - ], - [ - 'Anonymous function sometimes return something but return statement at the end is missing.', - 40, - ], - [ - 'Anonymous function should have native return typehint "array".', - 46, - ], - ]); - } - - public function testBug2682(): void - { - $this->analyse([__DIR__ . '/data/bug-2682.php'], [ - [ - 'Anonymous function should have native return typehint "void".', - 9, - ], - ]); - } - - public function testBug5164(): void - { - $this->analyse([__DIR__ . '/data/bug-5164.php'], [ - [ - 'Anonymous function should have native return typehint "Closure".', - 9, - ], - ]); - } - -} diff --git a/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php b/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php index 5966096e2f..3d3d3e1b6e 100644 --- a/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php +++ b/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php @@ -4,19 +4,21 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class MissingReturnRuleTest extends RuleTestCase { - /** @var bool */ - private $checkExplicitMixedMissingReturn; + private bool $checkExplicitMixedMissingReturn; + + private bool $checkPhpDocMissingReturn = true; protected function getRule(): Rule { - return new MissingReturnRule($this->checkExplicitMixedMissingReturn, true); + return new MissingReturnRule($this->checkExplicitMixedMissingReturn, $this->checkPhpDocMissingReturn); } public function testRule(): void @@ -143,4 +145,138 @@ public function testBug3669(): void $this->analyse([__DIR__ . '/data/bug-3669.php'], []); } + public function dataCheckPhpDocMissingReturn(): array + { + return [ + [ + true, + [ + [ + 'Method CheckPhpDocMissingReturn\Foo::doFoo() should return int|string but return statement is missing.', + 11, + ], + [ + 'Method CheckPhpDocMissingReturn\Foo::doFoo2() should return string|null but return statement is missing.', + 19, + ], + [ + 'Method CheckPhpDocMissingReturn\Foo::doFoo3() should return int|string but return statement is missing.', + 29, + ], + [ + 'Method CheckPhpDocMissingReturn\Foo::doFoo4() should return string|null but return statement is missing.', + 39, + ], + [ + 'Method CheckPhpDocMissingReturn\Foo::doFoo5() should return mixed but return statement is missing.', + 49, + ], + [ + 'Method CheckPhpDocMissingReturn\Bar::doFoo() should return int|string but return statement is missing.', + 59, + ], + [ + 'Method CheckPhpDocMissingReturn\Bar::doFoo2() should return string|null but return statement is missing.', + 64, + ], + [ + 'Method CheckPhpDocMissingReturn\Bar::doFoo3() should return int|string but return statement is missing.', + 71, + ], + [ + 'Method CheckPhpDocMissingReturn\Bar::doFoo4() should return string|null but return statement is missing.', + 78, + ], + [ + 'Method CheckPhpDocMissingReturn\Bar::doFoo5() should return mixed but return statement is missing.', + 85, + ], + ], + ], + [ + false, + [ + [ + 'Method CheckPhpDocMissingReturn\Foo::doFoo() should return int|string but return statement is missing.', + 11, + ], + [ + 'Method CheckPhpDocMissingReturn\Foo::doFoo3() should return int|string but return statement is missing.', + 29, + ], + [ + 'Method CheckPhpDocMissingReturn\Foo::doFoo5() should return mixed but return statement is missing.', + 49, + ], + [ + 'Method CheckPhpDocMissingReturn\Bar::doFoo() should return int|string but return statement is missing.', + 59, + ], + [ + 'Method CheckPhpDocMissingReturn\Bar::doFoo2() should return string|null but return statement is missing.', + 64, + ], + [ + 'Method CheckPhpDocMissingReturn\Bar::doFoo3() should return int|string but return statement is missing.', + 71, + ], + [ + 'Method CheckPhpDocMissingReturn\Bar::doFoo4() should return string|null but return statement is missing.', + 78, + ], + [ + 'Method CheckPhpDocMissingReturn\Bar::doFoo5() should return mixed but return statement is missing.', + 85, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataCheckPhpDocMissingReturn + * @param mixed[] $errors + */ + public function testCheckPhpDocMissingReturn(bool $checkPhpDocMissingReturn, array $errors): void + { + if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkExplicitMixedMissingReturn = true; + $this->checkPhpDocMissingReturn = $checkPhpDocMissingReturn; + $this->analyse([__DIR__ . '/data/check-phpdoc-missing-return.php'], $errors); + } + + public function dataModelMixin(): array + { + return [ + [ + true, + ], + [ + false, + ], + ]; + } + + /** + * @dataProvider dataModelMixin + */ + public function testModelMixin(bool $checkExplicitMixedMissingReturn): void + { + if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkExplicitMixedMissingReturn = $checkExplicitMixedMissingReturn; + $this->checkPhpDocMissingReturn = true; + $this->analyse([__DIR__ . '/../../Analyser/data/model-mixin.php'], [ + [ + 'Method ModelMixin\Model::__callStatic() should return mixed but return statement is missing.', + 13, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Missing/data/check-phpdoc-missing-return.php b/tests/PHPStan/Rules/Missing/data/check-phpdoc-missing-return.php new file mode 100644 index 0000000000..126b910f05 --- /dev/null +++ b/tests/PHPStan/Rules/Missing/data/check-phpdoc-missing-return.php @@ -0,0 +1,90 @@ += 8.0 + +namespace CheckPhpDocMissingReturn; + +class Foo +{ + + /** + * @return string|int + */ + public function doFoo() + { + + } + + /** + * @return string|null + */ + public function doFoo2() + { + + } + + /** + * @return string|int + */ + public function doFoo3() + { + if (rand()) { + return 'foo'; + } + } + + /** + * @return string|null + */ + public function doFoo4() + { + if (rand()) { + return 'foo'; + } + } + + /** + * @return mixed + */ + public function doFoo5() + { + if (rand()) { + return 'foo'; + } + } + +} + +class Bar +{ + + public function doFoo(): string|int + { + + } + + public function doFoo2(): ?string + { + + } + + public function doFoo3(): string|int + { + if (rand()) { + return 'foo'; + } + } + + public function doFoo4(): ?string + { + if (rand()) { + return 'foo'; + } + } + + public function doFoo5(): mixed + { + if (rand()) { + return 'foo'; + } + } + +} diff --git a/tests/PHPStan/Rules/Missing/data/missing-closure-native-return-typehint.php b/tests/PHPStan/Rules/Missing/data/missing-closure-native-return-typehint.php deleted file mode 100644 index 1eb7d37d14..0000000000 --- a/tests/PHPStan/Rules/Missing/data/missing-closure-native-return-typehint.php +++ /dev/null @@ -1,55 +0,0 @@ - 'bar', - ]; - - return $array; - }; - } - -} diff --git a/tests/PHPStan/Rules/Namespaces/ExistingNamesInGroupUseRuleTest.php b/tests/PHPStan/Rules/Namespaces/ExistingNamesInGroupUseRuleTest.php index 442d127776..481511cf87 100644 --- a/tests/PHPStan/Rules/Namespaces/ExistingNamesInGroupUseRuleTest.php +++ b/tests/PHPStan/Rules/Namespaces/ExistingNamesInGroupUseRuleTest.php @@ -3,17 +3,19 @@ namespace PHPStan\Rules\Namespaces; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingNamesInGroupUseRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingNamesInGroupUseRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { $broker = $this->createReflectionProvider(); - return new ExistingNamesInGroupUseRule($broker, new ClassCaseSensitivityCheck($broker), true); + return new ExistingNamesInGroupUseRule($broker, new ClassCaseSensitivityCheck($broker, true), true); } public function testRule(): void diff --git a/tests/PHPStan/Rules/Namespaces/ExistingNamesInUseRuleTest.php b/tests/PHPStan/Rules/Namespaces/ExistingNamesInUseRuleTest.php index dc0750e851..7389c4b58e 100644 --- a/tests/PHPStan/Rules/Namespaces/ExistingNamesInUseRuleTest.php +++ b/tests/PHPStan/Rules/Namespaces/ExistingNamesInUseRuleTest.php @@ -3,14 +3,16 @@ namespace PHPStan\Rules\Namespaces; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingNamesInUseRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingNamesInUseRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { $broker = $this->createReflectionProvider(); return new ExistingNamesInUseRule($broker, new ClassCaseSensitivityCheck($broker, true), true); diff --git a/tests/PHPStan/Rules/NodeConnectingRule.php b/tests/PHPStan/Rules/NodeConnectingRule.php index 9629e48986..86dd273d89 100644 --- a/tests/PHPStan/Rules/NodeConnectingRule.php +++ b/tests/PHPStan/Rules/NodeConnectingRule.php @@ -4,6 +4,8 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use function get_class; +use function sprintf; /** * @implements Rule @@ -23,7 +25,7 @@ public function processNode(Node $node, Scope $scope): array 'Parent: %s, previous: %s, next: %s', get_class($node->getAttribute('parent')), get_class($node->getAttribute('previous')), - get_class($node->getAttribute('next')) + get_class($node->getAttribute('next')), ), ]; } diff --git a/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php index e72e1f3364..0e88a2e23b 100644 --- a/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidBinaryOperationRuleTest.php @@ -2,19 +2,23 @@ namespace PHPStan\Rules\Operators; +use PhpParser\PrettyPrinter\Standard; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class InvalidBinaryOperationRuleTest extends \PHPStan\Testing\RuleTestCase +class InvalidBinaryOperationRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new InvalidBinaryOperationRule( - new \PhpParser\PrettyPrinter\Standard(), - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false) + new Standard(), + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false), ); } @@ -109,6 +113,134 @@ public function testRule(): void 'Binary operation "+" between stdClass and int results in an error.', 157, ], + [ + 'Binary operation "+" between non-empty-string and 10 results in an error.', + 184, + ], + [ + 'Binary operation "-" between non-empty-string and 10 results in an error.', + 185, + ], + [ + 'Binary operation "*" between non-empty-string and 10 results in an error.', + 186, + ], + [ + 'Binary operation "/" between non-empty-string and 10 results in an error.', + 187, + ], + [ + 'Binary operation "+" between 10 and non-empty-string results in an error.', + 189, + ], + [ + 'Binary operation "-" between 10 and non-empty-string results in an error.', + 190, + ], + [ + 'Binary operation "*" between 10 and non-empty-string results in an error.', + 191, + ], + [ + 'Binary operation "/" between 10 and non-empty-string results in an error.', + 192, + ], + [ + 'Binary operation "+" between string and 10 results in an error.', + 194, + ], + [ + 'Binary operation "-" between string and 10 results in an error.', + 195, + ], + [ + 'Binary operation "*" between string and 10 results in an error.', + 196, + ], + [ + 'Binary operation "/" between string and 10 results in an error.', + 197, + ], + [ + 'Binary operation "+" between 10 and string results in an error.', + 199, + ], + [ + 'Binary operation "-" between 10 and string results in an error.', + 200, + ], + [ + 'Binary operation "*" between 10 and string results in an error.', + 201, + ], + [ + 'Binary operation "/" between 10 and string results in an error.', + 202, + ], + [ + 'Binary operation "+" between class-string and 10 results in an error.', + 204, + ], + [ + 'Binary operation "-" between class-string and 10 results in an error.', + 205, + ], + [ + 'Binary operation "*" between class-string and 10 results in an error.', + 206, + ], + [ + 'Binary operation "/" between class-string and 10 results in an error.', + 207, + ], + [ + 'Binary operation "+" between 10 and class-string results in an error.', + 209, + ], + [ + 'Binary operation "-" between 10 and class-string results in an error.', + 210, + ], + [ + 'Binary operation "*" between 10 and class-string results in an error.', + 211, + ], + [ + 'Binary operation "/" between 10 and class-string results in an error.', + 212, + ], + [ + 'Binary operation "+" between literal-string and 10 results in an error.', + 214, + ], + [ + 'Binary operation "-" between literal-string and 10 results in an error.', + 215, + ], + [ + 'Binary operation "*" between literal-string and 10 results in an error.', + 216, + ], + [ + 'Binary operation "/" between literal-string and 10 results in an error.', + 217, + ], + [ + 'Binary operation "+" between 10 and literal-string results in an error.', + 219, + ], + [ + 'Binary operation "-" between 10 and literal-string results in an error.', + 220, + ], + [ + 'Binary operation "*" between 10 and literal-string results in an error.', + 221, + ], + [ + 'Binary operation "/" between 10 and literal-string results in an error.', + 222, + ], ]); } @@ -122,4 +254,18 @@ public function testBug3515(): void $this->analyse([__DIR__ . '/data/bug-3515.php'], []); } + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/invalid-binary-nullsafe.php'], [ + [ + 'Binary operation "+" between array|null and \'2\' results in an error.', + 12, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Operators/InvalidComparisonOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidComparisonOperationRuleTest.php index ef493ca348..03f6bece97 100644 --- a/tests/PHPStan/Rules/Operators/InvalidComparisonOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidComparisonOperationRuleTest.php @@ -2,18 +2,21 @@ namespace PHPStan\Rules\Operators; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class InvalidComparisonOperationRuleTest extends \PHPStan\Testing\RuleTestCase +class InvalidComparisonOperationRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new InvalidComparisonOperationRule( - new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false) + new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false), ); } @@ -143,4 +146,18 @@ public function testRule(): void ]); } + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/invalid-comparison-nullsafe.php'], [ + [ + 'Comparison operation "==" between stdClass|null and int results in an error.', + 12, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Operators/InvalidIncDecOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidIncDecOperationRuleTest.php index a5564251cd..7eda97b50c 100644 --- a/tests/PHPStan/Rules/Operators/InvalidIncDecOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidIncDecOperationRuleTest.php @@ -2,13 +2,16 @@ namespace PHPStan\Rules\Operators; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class InvalidIncDecOperationRuleTest extends \PHPStan\Testing\RuleTestCase +class InvalidIncDecOperationRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new InvalidIncDecOperationRule(false); } diff --git a/tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php b/tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php index 77bb4fe9e3..fbc6a265f9 100644 --- a/tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php +++ b/tests/PHPStan/Rules/Operators/InvalidUnaryOperationRuleTest.php @@ -2,13 +2,16 @@ namespace PHPStan\Rules\Operators; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class InvalidUnaryOperationRuleTest extends \PHPStan\Testing\RuleTestCase +class InvalidUnaryOperationRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new InvalidUnaryOperationRule(); } @@ -33,7 +36,7 @@ public function testRule(): void 20, ], [ - 'Unary operation "~" on array() results in an error.', + 'Unary operation "~" on array{} results in an error.', 24, ], ]); diff --git a/tests/PHPStan/Rules/Operators/data/invalid-binary-nullsafe.php b/tests/PHPStan/Rules/Operators/data/invalid-binary-nullsafe.php new file mode 100644 index 0000000000..5227e1fcac --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/invalid-binary-nullsafe.php @@ -0,0 +1,13 @@ += 8.0 + +namespace InvalidBinaryNullsafe; + +class Bar +{ + public array $array; +} + +function dooFoo(?Bar $bar) +{ + $bar?->array + '2'; +} diff --git a/tests/PHPStan/Rules/Operators/data/invalid-binary.php b/tests/PHPStan/Rules/Operators/data/invalid-binary.php index 9ea75a0589..3069c82ce8 100644 --- a/tests/PHPStan/Rules/Operators/data/invalid-binary.php +++ b/tests/PHPStan/Rules/Operators/data/invalid-binary.php @@ -173,3 +173,84 @@ function (array $args) { function (array $args) { isset($args['y']) ? $args + [] : $args; }; + +/** + * @param non-empty-string $foo + * @param string $bar + * @param class-string $foobar + * @param literal-string $literalString + */ +function bug6624_should_error($foo, $bar, $foobar, $literalString) { + echo ($foo + 10); + echo ($foo - 10); + echo ($foo * 10); + echo ($foo / 10); + + echo (10 + $foo); + echo (10 - $foo); + echo (10 * $foo); + echo (10 / $foo); + + echo ($bar + 10); + echo ($bar - 10); + echo ($bar * 10); + echo ($bar / 10); + + echo (10 + $bar); + echo (10 - $bar); + echo (10 * $bar); + echo (10 / $bar); + + echo ($foobar + 10); + echo ($foobar - 10); + echo ($foobar * 10); + echo ($foobar / 10); + + echo (10 + $foobar); + echo (10 - $foobar); + echo (10 * $foobar); + echo (10 / $foobar); + + echo ($literalString + 10); + echo ($literalString - 10); + echo ($literalString * 10); + echo ($literalString / 10); + + echo (10 + $literalString); + echo (10 - $literalString); + echo (10 * $literalString); + echo (10 / $literalString); +} + +/** + * @param numeric-string $numericString + */ +function bug6624_no_error($numericString) { + echo ($numericString + 10); + echo ($numericString - 10); + echo ($numericString * 10); + echo ($numericString / 10); + + echo (10 + $numericString); + echo (10 - $numericString); + echo (10 * $numericString); + echo (10 / $numericString); + + $numericLiteral = "123"; + + echo ($numericLiteral + 10); + echo ($numericLiteral - 10); + echo ($numericLiteral * 10); + echo ($numericLiteral / 10); + + echo (10 + $numericLiteral); + echo (10 - $numericLiteral); + echo (10 * $numericLiteral); + echo (10 / $numericLiteral); +} + +function benevolentPlus(array $a, int $i): void { + foreach ($a as $k => $v) { + echo $k + $i; + } +}; diff --git a/tests/PHPStan/Rules/Operators/data/invalid-comparison-nullsafe.php b/tests/PHPStan/Rules/Operators/data/invalid-comparison-nullsafe.php new file mode 100644 index 0000000000..e3c5338507 --- /dev/null +++ b/tests/PHPStan/Rules/Operators/data/invalid-comparison-nullsafe.php @@ -0,0 +1,13 @@ += 8.0 + +namespace InvalidComparisonNullsafe; + +class Bar +{ + public \stdClass $val; +} + +function doFoo(?Bar $bar, int $a) +{ + $bar?->val == $a; +} diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRuleTest.php new file mode 100644 index 0000000000..71e78e7ef6 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRuleTest.php @@ -0,0 +1,46 @@ + + */ +class IncompatibleClassConstantPhpDocTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new IncompatibleClassConstantPhpDocTypeRule(new GenericObjectTypeCheck(), new UnresolvableTypeHelper()); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/incompatible-class-constant-phpdoc.php'], [ + [ + '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, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php index cfdf2131fc..d7cf8d5470 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php @@ -3,23 +3,23 @@ namespace PHPStan\Rules\PhpDoc; use PHPStan\Rules\Generics\GenericObjectTypeCheck; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class IncompatiblePhpDocTypeRuleTest extends \PHPStan\Testing\RuleTestCase +class IncompatiblePhpDocTypeRuleTest extends RuleTestCase { - /** @var bool */ - private $deepInspectTypes = false; - - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new IncompatiblePhpDocTypeRule( self::getContainer()->getByType(FileTypeMapper::class), new GenericObjectTypeCheck(), - new UnresolvableTypeHelper($this->deepInspectTypes) + new UnresolvableTypeHelper(), ); } @@ -73,14 +73,17 @@ public function testRule(): void [ 'PHPDoc tag @param for parameter $a with type T is not subtype of native type int.', 154, + 'Write @template T of int to fix this.', ], [ 'PHPDoc tag @param for parameter $b with type U of DateTimeInterface is not subtype of native type DateTime.', 154, + 'Write @template U of DateTime to fix this.', ], [ - 'PHPDoc tag @return with type DateTimeInterface is not subtype of native type DateTime.', + 'PHPDoc tag @return with type U of DateTimeInterface is not subtype of native type DateTime.', 154, + 'Write @template U of DateTime to fix this.', ], [ 'PHPDoc tag @param for parameter $foo contains generic type InvalidPhpDocDefinitions\Foo but class InvalidPhpDocDefinitions\Foo is not generic.', @@ -146,6 +149,11 @@ public function testRule(): void 'PHPDoc tag @return contains generic type InvalidPhpDocDefinitions\Foo but class InvalidPhpDocDefinitions\Foo is not generic.', 274, ], + [ + 'PHPDoc tag @param for parameter $i with type TFoo is not subtype of native type int.', + 283, + 'Write @template TFoo of int to fix this.', + ], ]); } @@ -156,7 +164,6 @@ public function testBug4643(): void public function testBug3753(): void { - $this->deepInspectTypes = true; $this->analyse([__DIR__ . '/data/bug-3753.php'], [ [ 'PHPDoc tag @param for parameter $foo contains unresolvable type.', @@ -169,9 +176,32 @@ public function testBug3753(): void ]); } - public function testBug3753NotDeepInspectTypes(): void + public function testTemplateTypeNativeTypeObject(): void { - $this->analyse([__DIR__ . '/data/bug-3753.php'], []); + if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + $this->analyse([__DIR__ . '/data/template-type-native-type-object.php'], [ + [ + 'PHPDoc tag @return with type T is not subtype of native type object.', + 23, + 'Write @template T of object to fix this.', + ], + ]); + } + + public function testEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/generic-enum-param.php'], [ + [ + 'PHPDoc tag @param for parameter $e contains generic type GenericEnumParam\FooEnum but enum GenericEnumParam\FooEnum is not generic.', + 16, + ], + ]); } } diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php index 42ee5eee86..80cd2c3ed9 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php @@ -4,16 +4,18 @@ use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class IncompatiblePropertyPhpDocTypeRuleTest extends \PHPStan\Testing\RuleTestCase +class IncompatiblePropertyPhpDocTypeRuleTest extends RuleTestCase { protected function getRule(): Rule { - return new IncompatiblePropertyPhpDocTypeRule(new GenericObjectTypeCheck(), new UnresolvableTypeHelper(true)); + return new IncompatiblePropertyPhpDocTypeRule(new GenericObjectTypeCheck(), new UnresolvableTypeHelper()); } public function testRule(): void @@ -76,6 +78,11 @@ public function testNativeTypes(): void 'PHPDoc tag @var for property IncompatiblePhpDocPropertyNativeType\Foo::$stringOrInt with type int|string is not subtype of native type string.', 21, ], + [ + 'PHPDoc tag @var for property IncompatiblePhpDocPropertyNativeType\Lorem::$string with type T is not subtype of native type string.', + 45, + 'Write @template T of string to fix this.', + ], ]); } diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php index 0f3ee70c93..fda7477219 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php @@ -4,18 +4,20 @@ use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\Parser\PhpDocParser; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class InvalidPHPStanDocTagRuleTest extends \PHPStan\Testing\RuleTestCase +class InvalidPHPStanDocTagRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new InvalidPHPStanDocTagRule( self::getContainer()->getByType(Lexer::class), - self::getContainer()->getByType(PhpDocParser::class) + self::getContainer()->getByType(PhpDocParser::class), ); } diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleTest.php index 11c4ac4650..f3d5726d44 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleTest.php @@ -4,18 +4,20 @@ use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\Parser\PhpDocParser; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class InvalidPhpDocTagValueRuleTest extends \PHPStan\Testing\RuleTestCase +class InvalidPhpDocTagValueRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new InvalidPhpDocTagValueRule( self::getContainer()->getByType(Lexer::class), - self::getContainer()->getByType(PhpDocParser::class) + self::getContainer()->getByType(PhpDocParser::class), ); } @@ -86,6 +88,10 @@ public function testRule(): void '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, + ], ]); } diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php index 1512dc6309..f4442b4005 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php @@ -8,9 +8,10 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class InvalidPhpDocVarTagTypeRuleTest extends RuleTestCase { @@ -21,12 +22,12 @@ protected function getRule(): Rule return new InvalidPhpDocVarTagTypeRule( self::getContainer()->getByType(FileTypeMapper::class), $broker, - new ClassCaseSensitivityCheck($broker), + new ClassCaseSensitivityCheck($broker, true), new GenericObjectTypeCheck(), - new MissingTypehintCheck($broker, true, true, true), - new UnresolvableTypeHelper(true), + new MissingTypehintCheck($broker, true, true, true, []), + new UnresolvableTypeHelper(), + true, true, - true ); } @@ -104,12 +105,12 @@ public function testBug4486(): void { $this->analyse([__DIR__ . '/data/bug-4486.php'], [ [ - 'PHPDoc tag @var for variable $one contains unknown class Bug4486\ClassName1.', + 'PHPDoc tag @var for variable $one contains unknown class Some\Namespaced\ClassName1.', 10, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], [ - 'PHPDoc tag @var for variable $two contains unknown class Bug4486\ClassName2.', + 'PHPDoc tag @var for variable $two contains unknown class Some\Namespaced\ClassName2.', 10, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], @@ -125,7 +126,7 @@ public function testBug4486Namespace(): void { $this->analyse([__DIR__ . '/data/bug-4486-ns.php'], [ [ - 'PHPDoc tag @var for variable $one contains unknown class ClassName1.', + 'PHPDoc tag @var for variable $one contains unknown class Bug4486Namespace\ClassName1.', 6, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], @@ -137,4 +138,20 @@ public function testBug4486Namespace(): void ]); } + public function testBug6252(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + $this->analyse([__DIR__ . '/data/bug-6252.php'], []); + } + + public function testBug6348(): void + { + if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + $this->analyse([__DIR__ . '/data/bug-6348.php'], []); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidThrowsPhpDocValueRuleTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidThrowsPhpDocValueRuleTest.php index 5d3b9b1e46..90120196da 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidThrowsPhpDocValueRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidThrowsPhpDocValueRuleTest.php @@ -2,16 +2,21 @@ namespace PHPStan\Rules\PhpDoc; +use InvalidThrowsPhpDocMergeInherited\Four; +use InvalidThrowsPhpDocMergeInherited\Three; +use InvalidThrowsPhpDocMergeInherited\Two; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\VerbosityLevel; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class InvalidThrowsPhpDocValueRuleTest extends \PHPStan\Testing\RuleTestCase +class InvalidThrowsPhpDocValueRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new InvalidThrowsPhpDocValueRule(self::getContainer()->getByType(FileTypeMapper::class)); } @@ -76,17 +81,17 @@ public function dataMergeInheritedPhpDocs(): array { return [ [ - \InvalidThrowsPhpDocMergeInherited\Two::class, + Two::class, 'method', 'InvalidThrowsPhpDocMergeInherited\C|InvalidThrowsPhpDocMergeInherited\D', ], [ - \InvalidThrowsPhpDocMergeInherited\Three::class, + Three::class, 'method', 'InvalidThrowsPhpDocMergeInherited\C|InvalidThrowsPhpDocMergeInherited\D', ], [ - \InvalidThrowsPhpDocMergeInherited\Four::class, + Four::class, 'method', 'InvalidThrowsPhpDocMergeInherited\C|InvalidThrowsPhpDocMergeInherited\D', ], @@ -95,17 +100,14 @@ public function dataMergeInheritedPhpDocs(): array /** * @dataProvider dataMergeInheritedPhpDocs - * @param string $className - * @param string $method - * @param string $expectedType */ public function testMergeInheritedPhpDocs( string $className, string $method, - string $expectedType + string $expectedType, ): void { - $reflectionProvider = $this->createBroker(); + $reflectionProvider = $this->createReflectionProvider(); $reflection = $reflectionProvider->getClass($className); $method = $reflection->getNativeMethod($method); $throwsType = $method->getThrowType(); diff --git a/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php b/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php index 05d47dbc03..63164b9bda 100644 --- a/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php @@ -5,9 +5,10 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\FileTypeMapper; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class WrongVariableNameInVarTagRuleTest extends RuleTestCase { @@ -16,7 +17,6 @@ protected function getRule(): Rule { return new WrongVariableNameInVarTagRule( self::getContainer()->getByType(FileTypeMapper::class), - true ); } @@ -174,4 +174,18 @@ public function testBug4505(): void $this->analyse([__DIR__ . '/data/bug-4505.php'], []); } + public function testEnums(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('This test needs PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/wrong-var-enum.php'], [ + [ + 'PHPDoc tag @var above an enum has no effect.', + 13, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-6252.php b/tests/PHPStan/Rules/PhpDoc/data/bug-6252.php new file mode 100644 index 0000000000..9d9416fc77 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-6252.php @@ -0,0 +1,15 @@ +#!/usr/bin/env php += 8.1 + +namespace GenericEnumParam; + +enum FooEnum +{ + +} + +class Foo +{ + + /** + * @param FooEnum $e + */ + public function doFoo(FooEnum $e): void + { + + } + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-class-constant-phpdoc.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-class-constant-phpdoc.php new file mode 100644 index 0000000000..c4f163e773 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-class-constant-phpdoc.php @@ -0,0 +1,52 @@ + */ + 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-native-types.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-native-types.php index 93a4a38a91..56c5f7bc60 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-native-types.php +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-property-native-types.php @@ -36,3 +36,12 @@ class Baz private string $stringProp; } + +/** @template T */ +class Lorem +{ + + /** @var T */ + private string $string; + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php b/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php index 517a3e0ac3..5f7fd45b73 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php +++ b/tests/PHPStan/Rules/PhpDoc/data/incompatible-types.php @@ -275,3 +275,12 @@ function genericNestedNonTemplateArgs() { } + +/** + * @template TFoo + * @param TFoo $i + */ +function genericWrongBound(int $i) +{ + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc.php index 7d9b462f0d..15a61e603e 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc.php +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc.php @@ -73,3 +73,11 @@ public function doFoo() } } + +class ClassConstant +{ + + /** @var (Foo|Bar */ + const FOO = 1; + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/invalid-var-tag-type.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-var-tag-type.php index 1c1991710a..fd98bab42f 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/invalid-var-tag-type.php +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-var-tag-type.php @@ -83,3 +83,11 @@ class Bar private $foo; } + +class Baz +{ + + /** @var self&\stdClass */ + const FOO = 1; + +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/template-type-native-type-object.php b/tests/PHPStan/Rules/PhpDoc/data/template-type-native-type-object.php new file mode 100644 index 0000000000..823fb21600 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/template-type-native-type-object.php @@ -0,0 +1,31 @@ += 7.4 + +namespace TemplateTypeNativeTypeObject; + +class HelloWorld +{ + /** + * @var array + */ + private array $instances; + + public function __construct() + { + $this->instances = []; + } + + /** + * @phpstan-template T + * @phpstan-param class-string $className + * + * @phpstan-return T + */ + public function getInstanceByName(string $className, string $name): object + { + $instance = $this->instances["[{$className}]{$name}"]; + + \assert($instance instanceof $className); + + return $instance; + } +} diff --git a/tests/PHPStan/Rules/PhpDoc/data/wrong-var-enum.php b/tests/PHPStan/Rules/PhpDoc/data/wrong-var-enum.php new file mode 100644 index 0000000000..6153fc4db6 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/wrong-var-enum.php @@ -0,0 +1,16 @@ += 8.1 + +namespace WrongVarEnum; + +enum Foo +{ + +} + +/** + * @var Foo $test + */ +enum Bar +{ + +} diff --git a/tests/PHPStan/Rules/Properties/AccessPrivatePropertyThroughStaticRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPrivatePropertyThroughStaticRuleTest.php new file mode 100644 index 0000000000..09ce325b1a --- /dev/null +++ b/tests/PHPStan/Rules/Properties/AccessPrivatePropertyThroughStaticRuleTest.php @@ -0,0 +1,27 @@ + */ +class AccessPrivatePropertyThroughStaticRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new AccessPrivatePropertyThroughStaticRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/access-private-property-static.php'], [ + [ + 'Unsafe access to private property AccessPrivatePropertyThroughStatic\Foo::$foo through static::.', + 13, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php index 6554f967ed..e1a4f4b73e 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php @@ -5,18 +5,19 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class AccessPropertiesInAssignRuleTest extends RuleTestCase { protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new AccessPropertiesInAssignRule( - new AccessPropertiesRule($broker, new RuleLevelHelper($broker, true, false, true, false), true) + new AccessPropertiesRule($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, false, true, false), true), ); } @@ -25,6 +26,23 @@ public function testRule(): void $this->analyse([__DIR__ . '/data/access-properties-assign.php'], [ [ 'Access to an undefined property TestAccessPropertiesAssign\AccessPropertyWithDimFetch::$foo.', + 10, + ], + [ + 'Access to an undefined property TestAccessPropertiesAssign\AccessPropertyWithDimFetch::$foo.', + 15, + ], + ]); + } + + public function testRuleAssignOp(): void + { + if (PHP_VERSION_ID < 70400) { + self::markTestSkipped('Test requires PHP 7.4.'); + } + $this->analyse([__DIR__ . '/data/access-properties-assign-op.php'], [ + [ + 'Access to an undefined property TestAccessProperties\AssignOpNonexistentProperty::$flags.', 15, ], ]); diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php index cb843caafc..582c681e92 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php @@ -2,24 +2,25 @@ namespace PHPStan\Rules\Properties; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class AccessPropertiesRuleTest extends \PHPStan\Testing\RuleTestCase +class AccessPropertiesRuleTest extends RuleTestCase { - /** @var bool */ - private $checkThisOnly; + private bool $checkThisOnly; - /** @var bool */ - private $checkUnionTypes; + private bool $checkUnionTypes; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); - return new AccessPropertiesRule($broker, new RuleLevelHelper($broker, true, $this->checkThisOnly, $this->checkUnionTypes, false), true); + $reflectionProvider = $this->createReflectionProvider(); + return new AccessPropertiesRule($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, $this->checkThisOnly, $this->checkUnionTypes, false), true); } public function testAccessProperties(): void @@ -156,10 +157,6 @@ public function testAccessProperties(): void 'Access to an undefined property class@anonymous/tests/PHPStan/Rules/Properties/data/access-properties.php:294::$barProperty.', 299, ], - [ - 'Access to an undefined property TestAccessProperties\AccessPropertyWithDimFetch::$foo.', - 364, - ], [ 'Access to an undefined property TestAccessProperties\AccessInIsset::$foo.', 386, @@ -172,7 +169,7 @@ public function testAccessProperties(): void 'Cannot access property $array on stdClass|null.', 412, ], - ] + ], ); } @@ -294,18 +291,29 @@ public function testAccessPropertiesWithoutUnionTypes(): void 'Access to an undefined property class@anonymous/tests/PHPStan/Rules/Properties/data/access-properties.php:294::$barProperty.', 299, ], - [ - 'Access to an undefined property TestAccessProperties\AccessPropertyWithDimFetch::$foo.', - 364, - ], [ 'Access to an undefined property TestAccessProperties\AccessInIsset::$foo.', 386, ], - ] + ], ); } + public function testRuleAssignOp(): void + { + if (PHP_VERSION_ID < 70400) { + self::markTestSkipped('Test requires PHP 7.4.'); + } + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/access-properties-assign-op.php'], [ + [ + 'Access to an undefined property TestAccessProperties\AssignOpNonexistentProperty::$flags.', + 10, + ], + ]); + } + public function testAccessPropertiesOnThisOnly(): void { $this->checkThisOnly = true; @@ -321,15 +329,11 @@ public function testAccessPropertiesOnThisOnly(): void 'Access to private property $foo of parent class TestAccessProperties\FooAccessProperties.', 24, ], - [ - 'Access to an undefined property TestAccessProperties\AccessPropertyWithDimFetch::$foo.', - 364, - ], [ 'Access to an undefined property TestAccessProperties\AccessInIsset::$foo.', 386, ], - ] + ], ); } @@ -491,4 +495,62 @@ public function testBug4808(): void $this->analyse([__DIR__ . '/data/bug-4808.php'], []); } + + public function testBug5868(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-5868.php'], [ + [ + 'Cannot access property $child on Bug5868PropertyFetch\Foo|null.', + 31, + ], + [ + 'Cannot access property $child on Bug5868PropertyFetch\Child|null.', + 32, + ], + [ + 'Cannot access property $existingChild on Bug5868PropertyFetch\Child|null.', + 33, + ], + [ + 'Cannot access property $existingChild on Bug5868PropertyFetch\Child|null.', + 34, + ], + ]); + } + + public function testBug6385(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-6385.php'], [ + [ + 'Access to an undefined property UnitEnum::$value.', + 43, + ], + [ + 'Access to an undefined property Bug6385\ActualUnitEnum::$value.', + 47, + ], + ]); + } + + public function testBug6566(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-6566.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php index 214fa443d7..3a9a495b7a 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php @@ -6,18 +6,19 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class AccessStaticPropertiesInAssignRuleTest extends RuleTestCase { protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new AccessStaticPropertiesInAssignRule( - new AccessStaticPropertiesRule($broker, new RuleLevelHelper($broker, true, false, true, false), new ClassCaseSensitivityCheck($broker)) + new AccessStaticPropertiesRule($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, false, true, false), new ClassCaseSensitivityCheck($reflectionProvider, true)), ); } @@ -26,6 +27,23 @@ public function testRule(): void $this->analyse([__DIR__ . '/data/access-static-properties-assign.php'], [ [ 'Access to an undefined static property TestAccessStaticPropertiesAssign\AccessStaticPropertyWithDimFetch::$foo.', + 10, + ], + [ + 'Access to an undefined static property TestAccessStaticPropertiesAssign\AccessStaticPropertyWithDimFetch::$foo.', + 15, + ], + ]); + } + + public function testRuleAssignOp(): void + { + if (PHP_VERSION_ID < 70400) { + self::markTestSkipped('Test requires PHP 7.4.'); + } + $this->analyse([__DIR__ . '/data/access-static-properties-assign-op.php'], [ + [ + 'Access to an undefined static property AccessStaticProperties\AssignOpNonexistentProperty::$flags.', 15, ], ]); diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php index 740455d680..44fe1c71fb 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php @@ -3,21 +3,24 @@ namespace PHPStan\Rules\Properties; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class AccessStaticPropertiesRuleTest extends \PHPStan\Testing\RuleTestCase +class AccessStaticPropertiesRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { - $broker = $this->createReflectionProvider(); + $reflectionProvider = $this->createReflectionProvider(); return new AccessStaticPropertiesRule( - $broker, - new RuleLevelHelper($broker, true, false, true, false), - new ClassCaseSensitivityCheck($broker) + $reflectionProvider, + new RuleLevelHelper($reflectionProvider, true, false, true, false), + new ClassCaseSensitivityCheck($reflectionProvider, true), ); } @@ -161,10 +164,6 @@ public function testAccessStaticProperties(): void 'Static access to instance property ClassOrString::$instanceProperty.', 152, ], - [ - 'Access to an undefined static property AccessPropertyWithDimFetch::$foo.', - 163, - ], [ 'Access to an undefined static property AccessInIsset::$foo.', 185, @@ -174,6 +173,27 @@ public function testAccessStaticProperties(): void 209, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], + [ + 'Static access to instance property AccessWithStatic::$bar.', + 223, + ], + [ + 'Access to an undefined static property static(AccessWithStatic)::$nonexistent.', + 224, + ], + ]); + } + + public function testRuleAssignOp(): void + { + if (PHP_VERSION_ID < 70400) { + self::markTestSkipped('Test requires PHP 7.4.'); + } + $this->analyse([__DIR__ . '/data/access-static-properties-assign-op.php'], [ + [ + 'Access to an undefined static property AccessStaticProperties\AssignOpNonexistentProperty::$flags.', + 10, + ], ]); } diff --git a/tests/PHPStan/Rules/Properties/DefaultValueTypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/DefaultValueTypesAssignedToPropertiesRuleTest.php index 42064cd3e8..9f9e006fcb 100644 --- a/tests/PHPStan/Rules/Properties/DefaultValueTypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/DefaultValueTypesAssignedToPropertiesRuleTest.php @@ -4,11 +4,12 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class DefaultValueTypesAssignedToPropertiesRuleTest extends \PHPStan\Testing\RuleTestCase +class DefaultValueTypesAssignedToPropertiesRuleTest extends RuleTestCase { protected function getRule(): Rule @@ -47,19 +48,11 @@ public function testDefaultValueForNativePropertyType(): void ]); } - public function testDefaultValueForPromotedProperty(): void + public function testBug5607(): void { - if (!self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires static reflection.'); - } - - $this->analyse([__DIR__ . '/data/default-value-for-promoted-property.php'], [ - [ - 'Property DefaultValueForPromotedProperty\Foo::$foo (int) does not accept default value of type string.', - 9, - ], + $this->analyse([__DIR__ . '/data/bug-5607.php'], [ [ - 'Property DefaultValueForPromotedProperty\Foo::$foo (int) does not accept default value of type string.', + 'Property Bug5607\Cl::$u (Bug5607\A|null) does not accept default value of type array.', 10, ], ]); diff --git a/tests/PHPStan/Rules/Properties/DirectReadWritePropertiesExtensionProvider.php b/tests/PHPStan/Rules/Properties/DirectReadWritePropertiesExtensionProvider.php index bcfe73709f..6ce75189d7 100644 --- a/tests/PHPStan/Rules/Properties/DirectReadWritePropertiesExtensionProvider.php +++ b/tests/PHPStan/Rules/Properties/DirectReadWritePropertiesExtensionProvider.php @@ -5,15 +5,11 @@ class DirectReadWritePropertiesExtensionProvider implements ReadWritePropertiesExtensionProvider { - /** @var ReadWritePropertiesExtension[] */ - private $extensions; - /** * @param ReadWritePropertiesExtension[] $extensions */ - public function __construct(array $extensions) + public function __construct(private array $extensions) { - $this->extensions = $extensions; } /** diff --git a/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php index 99dea64d96..7623c72f7e 100644 --- a/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ExistingClassesInPropertiesRuleTest.php @@ -2,23 +2,31 @@ namespace PHPStan\Rules\Properties; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\ClassCaseSensitivityCheck; +use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ExistingClassesInPropertiesRuleTest extends \PHPStan\Testing\RuleTestCase +class ExistingClassesInPropertiesRuleTest extends RuleTestCase { + private int $phpVersion = PHP_VERSION_ID; + protected function getRule(): Rule { $broker = $this->createReflectionProvider(); return new ExistingClassesInPropertiesRule( $broker, - new ClassCaseSensitivityCheck($broker), + new ClassCaseSensitivityCheck($broker, true), + new UnresolvableTypeHelper(), + new PhpVersion($this->phpVersion), true, - false + false, ); } @@ -67,6 +75,10 @@ public function testNonexistentClass(): void 'Property PropertiesTypes\Foo::$withTrait has invalid type PropertiesTypes\SomeTrait.', 27, ], + [ + 'Class DateTime referenced with incorrect case: Datetime.', + 30, + ], [ 'Property PropertiesTypes\Foo::$nonexistentClassInGenericObjectType has unknown class PropertiesTypes\Foooo as its type.', 33, @@ -77,7 +89,7 @@ public function testNonexistentClass(): void 33, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], - ] + ], ); } @@ -134,4 +146,39 @@ public function testPromotedProperties(): void ]); } + public function dataIntersectionTypes(): array + { + return [ + [80000, []], + [ + 80100, + [ + [ + 'Property PropertyIntersectionTypes\Test::$prop2 has unresolvable native type.', + 30, + ], + [ + 'Property PropertyIntersectionTypes\Test::$prop3 has unresolvable native type.', + 32, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataIntersectionTypes + * @param mixed[] $errors + */ + public function testIntersectionTypes(int $phpVersion, array $errors): void + { + if (!self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->phpVersion = $phpVersion; + + $this->analyse([__DIR__ . '/data/intersection-types.php'], $errors); + } + } diff --git a/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php b/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php index 622295adab..ebea655346 100644 --- a/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Properties/MissingPropertyTypehintRuleTest.php @@ -3,32 +3,35 @@ namespace PHPStan\Rules\Properties; use PHPStan\Rules\MissingTypehintCheck; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class MissingPropertyTypehintRuleTest extends \PHPStan\Testing\RuleTestCase +class MissingPropertyTypehintRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { $broker = $this->createReflectionProvider(); - return new MissingPropertyTypehintRule(new MissingTypehintCheck($broker, true, true, true)); + return new MissingPropertyTypehintRule(new MissingTypehintCheck($broker, true, true, true, [])); } public function testRule(): void { $this->analyse([__DIR__ . '/data/missing-property-typehint.php'], [ [ - 'Property MissingPropertyTypehint\MyClass::$prop1 has no typehint specified.', + 'Property MissingPropertyTypehint\MyClass::$prop1 has no type specified.', 7, ], [ - 'Property MissingPropertyTypehint\MyClass::$prop2 has no typehint specified.', + 'Property MissingPropertyTypehint\MyClass::$prop2 has no type specified.', 9, ], [ - 'Property MissingPropertyTypehint\MyClass::$prop3 has no typehint specified.', + 'Property MissingPropertyTypehint\MyClass::$prop3 has no type specified.', 14, ], [ @@ -65,7 +68,7 @@ public function testPromotedProperties(): void } $this->analyse([__DIR__ . '/data/promoted-properties-missing-typehint.php'], [ [ - 'Property PromotedPropertiesMissingTypehint\Foo::$lorem has no typehint specified.', + 'Property PromotedPropertiesMissingTypehint\Foo::$lorem has no type specified.', 15, ], [ diff --git a/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php new file mode 100644 index 0000000000..71e1c09473 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php @@ -0,0 +1,60 @@ + + */ +class MissingReadOnlyPropertyAssignRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new MissingReadOnlyPropertyAssignRule([ + 'MissingReadOnlyPropertyAssign\\TestCase::setUp', + ]); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/missing-readonly-property-assign.php'], [ + [ + 'Class MissingReadOnlyPropertyAssign\Foo has an uninitialized readonly property $unassigned. Assign it in the constructor.', + 14, + ], + [ + 'Class MissingReadOnlyPropertyAssign\Foo has an uninitialized readonly property $unassigned2. Assign it in the constructor.', + 16, + ], + [ + 'Access to an uninitialized readonly property MissingReadOnlyPropertyAssign\Foo::$readBeforeAssigned.', + 33, + ], + [ + 'Readonly property MissingReadOnlyPropertyAssign\Foo::$doubleAssigned is already assigned.', + 37, + ], + [ + 'Class MissingReadOnlyPropertyAssign\BarDoubleAssignInSetter has an uninitialized readonly property $foo. Assign it in the constructor.', + 53, + ], + [ + 'Access to an uninitialized readonly property MissingReadOnlyPropertyAssign\AssignOp::$foo.', + 85, + ], + [ + 'Access to an uninitialized readonly property MissingReadOnlyPropertyAssign\AssignOp::$bar.', + 87, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/NullsafePropertyFetchRuleTest.php b/tests/PHPStan/Rules/Properties/NullsafePropertyFetchRuleTest.php index 5ed217dd66..febeff7d62 100644 --- a/tests/PHPStan/Rules/Properties/NullsafePropertyFetchRuleTest.php +++ b/tests/PHPStan/Rules/Properties/NullsafePropertyFetchRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -30,4 +31,13 @@ public function testRule(): void ]); } + public function testBug6020(): void + { + if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-6020.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php new file mode 100644 index 0000000000..ab31ff4b23 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/OverridingPropertyRuleTest.php @@ -0,0 +1,166 @@ + + */ +class OverridingPropertyRuleTest extends RuleTestCase +{ + + private bool $reportMaybes; + + protected function getRule(): Rule + { + return new OverridingPropertyRule(true, $this->reportMaybes); + } + + public function testRule(): void + { + if (!self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires static reflection.'); + } + + $this->reportMaybes = true; + $this->analyse([__DIR__ . '/data/overriding-property.php'], [ + [ + 'Static property OverridingProperty\Bar::$protectedFoo overrides non-static property OverridingProperty\Foo::$protectedFoo.', + 25, + ], + [ + 'Non-static property OverridingProperty\Bar::$protectedStaticFoo overrides static property OverridingProperty\Foo::$protectedStaticFoo.', + 26, + ], + [ + 'Static property OverridingProperty\Bar::$publicFoo overrides non-static property OverridingProperty\Foo::$publicFoo.', + 28, + ], + [ + 'Non-static property OverridingProperty\Bar::$publicStaticFoo overrides static property OverridingProperty\Foo::$publicStaticFoo.', + 29, + ], + [ + 'Readonly property OverridingProperty\ReadonlyChild::$readWrite overrides readwrite property OverridingProperty\ReadonlyParent::$readWrite.', + 45, + ], + [ + 'Readwrite property OverridingProperty\ReadonlyChild::$readOnly overrides readonly property OverridingProperty\ReadonlyParent::$readOnly.', + 46, + ], + [ + 'Readonly property OverridingProperty\ReadonlyChild2::$readWrite overrides readwrite property OverridingProperty\ReadonlyParent::$readWrite.', + 55, + ], + [ + 'Readwrite property OverridingProperty\ReadonlyChild2::$readOnly overrides readonly property OverridingProperty\ReadonlyParent::$readOnly.', + 56, + ], + [ + 'Private property OverridingProperty\PrivateDolor::$protectedFoo overriding protected property OverridingProperty\Dolor::$protectedFoo should be protected or public.', + 76, + ], + [ + 'Private property OverridingProperty\PrivateDolor::$publicFoo overriding public property OverridingProperty\Dolor::$publicFoo should also be public.', + 77, + ], + [ + 'Private property OverridingProperty\PrivateDolor::$anotherPublicFoo overriding public property OverridingProperty\Dolor::$anotherPublicFoo should also be public.', + 78, + ], + [ + 'Protected property OverridingProperty\ProtectedDolor::$publicFoo overriding public property OverridingProperty\Dolor::$publicFoo should also be public.', + 87, + ], + [ + 'Protected property OverridingProperty\ProtectedDolor::$anotherPublicFoo overriding public property OverridingProperty\Dolor::$anotherPublicFoo should also be public.', + 88, + ], + [ + 'Property OverridingProperty\TypeChild::$withType overriding property OverridingProperty\Typed::$withType (int) should also have native type int.', + 125, + ], + [ + 'Property OverridingProperty\TypeChild::$withoutType (int) overriding property OverridingProperty\Typed::$withoutType should not have a native type.', + 126, + ], + [ + 'Type string of property OverridingProperty\Typed2Child::$foo is not the same as type int of overridden property OverridingProperty\Typed2::$foo.', + 142, + ], + [ + 'PHPDoc type 4 of property OverridingProperty\Typed2WithPhpDoc::$foo is not the same as PHPDoc type 1|2|3 of overridden property OverridingProperty\TypedWithPhpDoc::$foo.', + 158, + sprintf( + "You can fix 3rd party PHPDoc types with stub files:\n %s", + 'https://phpstan.org/user-guide/stub-files', + ), + ], + ]); + } + + public function dataRulePHPDocTypes(): array + { + $tip = sprintf( + "You can fix 3rd party PHPDoc types with stub files:\n %s", + 'https://phpstan.org/user-guide/stub-files', + ); + $tipWithOption = sprintf( + "You can fix 3rd party PHPDoc types with stub files:\n %s\n This error can be turned off by setting\n %s", + 'https://phpstan.org/user-guide/stub-files', + 'reportMaybesInPropertyPhpDocTypes: false in your %configurationFile%.', + ); + + return [ + [ + false, + [ + [ + 'PHPDoc type array of property OverridingPropertyPhpDoc\Bar::$arrayClassStrings is not covariant with PHPDoc type array of overridden property OverridingPropertyPhpDoc\Foo::$arrayClassStrings.', + 26, + $tip, + ], + [ + 'PHPDoc type int of property OverridingPropertyPhpDoc\Bar::$string is not covariant with PHPDoc type string of overridden property OverridingPropertyPhpDoc\Foo::$string.', + 29, + $tip, + ], + ], + ], + [ + true, + [ + [ + 'PHPDoc type array of property OverridingPropertyPhpDoc\Bar::$array is not the same as PHPDoc type array of overridden property OverridingPropertyPhpDoc\Foo::$array.', + 23, + $tipWithOption, + ], + [ + 'PHPDoc type array of property OverridingPropertyPhpDoc\Bar::$arrayClassStrings is not the same as PHPDoc type array of overridden property OverridingPropertyPhpDoc\Foo::$arrayClassStrings.', + 26, + $tip, + ], + [ + 'PHPDoc type int of property OverridingPropertyPhpDoc\Bar::$string is not the same as PHPDoc type string of overridden property OverridingPropertyPhpDoc\Foo::$string.', + 29, + $tip, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataRulePHPDocTypes + * @param mixed[] $errors + */ + public function testRulePHPDocTypes(bool $reportMaybes, array $errors): void + { + $this->reportMaybes = $reportMaybes; + $this->analyse([__DIR__ . '/data/overriding-property-phpdoc.php'], $errors); + } + +} diff --git a/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php b/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php index ea787b22d9..104ecb16ee 100644 --- a/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/PropertyAttributesRuleTest.php @@ -11,6 +11,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -25,18 +26,18 @@ protected function getRule(): Rule new AttributesCheck( $reflectionProvider, new FunctionCallParametersCheck( - new RuleLevelHelper($reflectionProvider, true, false, true), + new RuleLevelHelper($reflectionProvider, true, false, true, false), new NullsafeCheck(), new PhpVersion(80000), - new UnresolvableTypeHelper(true), + new UnresolvableTypeHelper(), + new PropertyReflectionFinder(), true, true, true, true, - true ), - new ClassCaseSensitivityCheck($reflectionProvider, false) - ) + new ClassCaseSensitivityCheck($reflectionProvider, false), + ), ); } diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRefRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRefRuleTest.php new file mode 100644 index 0000000000..c0dae45d97 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRefRuleTest.php @@ -0,0 +1,42 @@ + + */ +class ReadOnlyPropertyAssignRefRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ReadOnlyPropertyAssignRefRule(new PropertyReflectionFinder()); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/readonly-assign-ref.php'], [ + [ + 'Readonly property ReadOnlyPropertyAssignRef\Foo::$foo is assigned by reference.', + 14, + ], + [ + 'Readonly property ReadOnlyPropertyAssignRef\Foo::$bar is assigned by reference.', + 15, + ], + [ + 'Readonly property ReadOnlyPropertyAssignRef\Foo::$bar is assigned by reference.', + 26, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php new file mode 100644 index 0000000000..b1efad947d --- /dev/null +++ b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php @@ -0,0 +1,106 @@ + + */ +class ReadOnlyPropertyAssignRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ReadOnlyPropertyAssignRule(new PropertyReflectionFinder()); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/readonly-assign.php'], [ + [ + 'Readonly property ReadonlyPropertyAssign\Foo::$foo is assigned outside of the constructor.', + 21, + ], + [ + 'Readonly property ReadonlyPropertyAssign\Foo::$bar is assigned outside of its declaring class.', + 33, + ], + [ + 'Readonly property ReadonlyPropertyAssign\Foo::$baz is assigned outside of its declaring class.', + 34, + ], + [ + 'Readonly property ReadonlyPropertyAssign\Foo::$bar is assigned outside of its declaring class.', + 39, + ], + [ + 'Readonly property ReadonlyPropertyAssign\Foo::$baz is assigned outside of its declaring class.', + 46, + ], + [ + 'Readonly property ReadonlyPropertyAssign\FooArrays::$details is assigned outside of the constructor.', + 64, + ], + [ + 'Readonly property ReadonlyPropertyAssign\FooArrays::$details is assigned outside of the constructor.', + 65, + ], + [ + 'Readonly property ReadonlyPropertyAssign\NotThis::$foo is not assigned on $this.', + 90, + ], + [ + 'Readonly property ReadonlyPropertyAssign\PostInc::$foo is assigned outside of the constructor.', + 102, + ], + [ + 'Readonly property ReadonlyPropertyAssign\PostInc::$foo is assigned outside of the constructor.', + 103, + ], + [ + 'Readonly property ReadonlyPropertyAssign\PostInc::$foo is assigned outside of the constructor.', + 105, + ], + [ + 'Readonly property ReadonlyPropertyAssign\ListAssign::$foo is assigned outside of the constructor.', + 122, + ], + [ + 'Readonly property ReadonlyPropertyAssign\ListAssign::$foo is assigned outside of the constructor.', + 127, + ], + [ + 'Readonly property ReadonlyPropertyAssign\FooEnum::$name is assigned outside of the constructor.', + 140, + ], + [ + 'Readonly property ReadonlyPropertyAssign\FooEnum::$value is assigned outside of the constructor.', + 141, + ], + [ + 'Readonly property ReadonlyPropertyAssign\FooEnum::$name is assigned outside of its declaring class.', + 151, + ], + [ + '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, + ], + [ + 'Readonly property ReadonlyPropertyAssign\Foo::$baz is assigned outside of its declaring class.', + 163, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyRuleTest.php new file mode 100644 index 0000000000..99dfd8b766 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyRuleTest.php @@ -0,0 +1,84 @@ + + */ +class ReadOnlyPropertyRuleTest extends RuleTestCase +{ + + private int $phpVersionId; + + protected function getRule(): Rule + { + return new ReadOnlyPropertyRule(new PhpVersion($this->phpVersionId)); + } + + public function dataRule(): array + { + return [ + [ + 80000, + [ + [ + 'Readonly properties are supported only on PHP 8.1 and later.', + 8, + ], + [ + 'Readonly properties are supported only on PHP 8.1 and later.', + 9, + ], + [ + 'Readonly property must have a native type.', + 9, + ], + [ + 'Readonly properties are supported only on PHP 8.1 and later.', + 10, + ], + [ + 'Readonly property cannot have a default value.', + 10, + ], + [ + 'Readonly properties are supported only on PHP 8.1 and later.', + 16, + ], + ], + ], + [ + 80100, + [ + [ + 'Readonly property must have a native type.', + 9, + ], + [ + 'Readonly property cannot have a default value.', + 10, + ], + ], + ], + ]; + } + + /** + * @dataProvider dataRule + * @param mixed[] $errors + */ + public function testRule(int $phpVersionId, array $errors): void + { + if (!self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires static reflection.'); + } + + $this->phpVersionId = $phpVersionId; + $this->analyse([__DIR__ . '/data/read-only-property.php'], $errors); + } + +} diff --git a/tests/PHPStan/Rules/Properties/ReadingWriteOnlyPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/ReadingWriteOnlyPropertiesRuleTest.php index f496d70757..946fa2a776 100644 --- a/tests/PHPStan/Rules/Properties/ReadingWriteOnlyPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadingWriteOnlyPropertiesRuleTest.php @@ -2,18 +2,20 @@ namespace PHPStan\Rules\Properties; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ReadingWriteOnlyPropertiesRuleTest extends \PHPStan\Testing\RuleTestCase +class ReadingWriteOnlyPropertiesRuleTest extends RuleTestCase { - /** @var bool */ - private $checkThisOnly; + private bool $checkThisOnly; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new ReadingWriteOnlyPropertiesRule(new PropertyDescriptor(), new PropertyReflectionFinder(), new RuleLevelHelper($this->createReflectionProvider(), true, $this->checkThisOnly, true, false), $this->checkThisOnly); } diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleNoBleedingEdgeTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleNoBleedingEdgeTest.php new file mode 100644 index 0000000000..fe8c016469 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleNoBleedingEdgeTest.php @@ -0,0 +1,50 @@ + + */ +class TypesAssignedToPropertiesRuleNoBleedingEdgeTest extends RuleTestCase +{ + + private bool $checkExplicitMixed = false; + + protected function getRule(): Rule + { + return new TypesAssignedToPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed), new PropertyDescriptor(), new PropertyReflectionFinder()); + } + + public function testGenericObjectWithUnspecifiedTemplateTypes(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/generic-object-unspecified-template-types.php'], [ + [ + 'Property GenericObjectUnspecifiedTemplateTypes\Bar::$ints (GenericObjectUnspecifiedTemplateTypes\ArrayCollection) does not accept GenericObjectUnspecifiedTemplateTypes\ArrayCollection.', + 67, + ], + ]); + } + + public function testGenericObjectWithUnspecifiedTemplateTypesLevel8(): void + { + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/generic-object-unspecified-template-types.php'], [ + [ + 'Property GenericObjectUnspecifiedTemplateTypes\Bar::$ints (GenericObjectUnspecifiedTemplateTypes\ArrayCollection) does not accept GenericObjectUnspecifiedTemplateTypes\ArrayCollection.', + 67, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + // no bleeding edge + return []; + } + +} diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index c50283c221..d1a9331dc3 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -2,17 +2,22 @@ namespace PHPStan\Rules\Properties; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class TypesAssignedToPropertiesRuleTest extends \PHPStan\Testing\RuleTestCase +class TypesAssignedToPropertiesRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + private bool $checkExplicitMixed = false; + + protected function getRule(): Rule { - return new TypesAssignedToPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false), new PropertyDescriptor(), new PropertyReflectionFinder()); + return new TypesAssignedToPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, $this->checkExplicitMixed), new PropertyDescriptor(), new PropertyReflectionFinder()); } public function testTypesAssignedToProperties(): void @@ -78,6 +83,30 @@ public function testTypesAssignedToProperties(): void 'Property PropertiesAssignedTypes\AssignRefFoo::$stringProperty (string) does not accept int.', 312, ], + [ + 'Property PropertiesAssignedTypes\PostInc::$foo (int) does not accept int.', + 334, + ], + [ + 'Property PropertiesAssignedTypes\PostInc::$bar (int<3, max>) does not accept int<2, max>.', + 335, + ], + [ + 'Property PropertiesAssignedTypes\PostInc::$foo (int) does not accept int.', + 346, + ], + [ + 'Property PropertiesAssignedTypes\PostInc::$bar (int<3, max>) does not accept int<2, max>.', + 347, + ], + [ + 'Property PropertiesAssignedTypes\ListAssign::$foo (string) does not accept int.', + 360, + ], + [ + 'Property PropertiesAssignedTypes\AppendToArrayAccess::$collection2 (ArrayAccess&Countable) does not accept Countable.', + 376, + ], ]); } @@ -189,4 +218,201 @@ public function testBug3777(): void ]); } + public function testAppendendArrayKey(): void + { + $this->analyse([__DIR__ . '/../Arrays/data/appended-array-key.php'], [ + [ + 'Property AppendedArrayKey\Foo::$intArray (array) does not accept array.', + 27, + ], + [ + 'Property AppendedArrayKey\Foo::$intArray (array) does not accept array.', + 28, + ], + [ + 'Property AppendedArrayKey\Foo::$intArray (array) does not accept array.', + 30, + ], + [ + 'Property AppendedArrayKey\Foo::$stringArray (array) does not accept array.', + 31, + ], + [ + 'Property AppendedArrayKey\Foo::$stringArray (array) does not accept array.', + 33, + ], + [ + 'Property AppendedArrayKey\Foo::$stringArray (array) does not accept array.', + 38, + ], + [ + 'Property AppendedArrayKey\Foo::$stringArray (array) does not accept array.', + 46, + ], + [ + 'Property AppendedArrayKey\MorePreciseKey::$test (array<1|2|3, string>) does not accept non-empty-array.', + 80, + ], + [ + 'Property AppendedArrayKey\MorePreciseKey::$test (array<1|2|3, string>) does not accept non-empty-array<1|2|3|4, string>.', + 85, + ], + ]); + } + + public function testBug5372Two(): void + { + $this->analyse([__DIR__ . '/../Arrays/data/bug-5372_2.php'], []); + } + + public function testBug5447(): void + { + $this->analyse([__DIR__ . '/../Arrays/data/bug-5447.php'], []); + } + + public function testAppendedArrayItemType(): void + { + $this->analyse( + [__DIR__ . '/../Arrays/data/appended-array-item.php'], + [ + [ + 'Property AppendedArrayItem\Foo::$integers (array) does not accept array.', + 18, + ], + [ + 'Property AppendedArrayItem\Foo::$callables (array) does not accept non-empty-array.', + 20, + ], + [ + 'Property AppendedArrayItem\Foo::$callables (array) does not accept non-empty-array.', + 23, + ], + [ + 'Property AppendedArrayItem\Foo::$callables (array) does not accept non-empty-array.', + 25, + ], + [ + 'Property AppendedArrayItem\Foo::$integers (array) does not accept array.', + 27, + ], + [ + 'Property AppendedArrayItem\Foo::$integers (array) does not accept array.', + 32, + ], + [ + 'Property AppendedArrayItem\Bar::$stringCallables (array) does not accept non-empty-array<(callable(): string)|(Closure(): 1)>.', + 45, + ], + [ + 'Property AppendedArrayItem\Baz::$staticProperty (array) does not accept array.', + 79, + ], + ], + ); + } + + public function testBug5804(): void + { + $this->analyse([__DIR__ . '/data/bug-5804.php'], [ + [ + 'Property Bug5804\Blah::$value (array|null) does not accept array.', + 12, + ], + [ + 'Property Bug5804\Blah::$value (array|null) does not accept array.', + 17, + ], + ]); + } + + public function testBug6286(): void + { + if (PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + $this->analyse([__DIR__ . '/data/bug-6286.php'], [ + [ + 'Property Bug6286\HelloWorld::$details (array{name: string, age: int}) does not accept array{name: string, age: \'Forty-two\'}.', + 19, + ], + [ + 'Property Bug6286\HelloWorld::$nestedDetails (array) does not accept non-empty-array.', + 22, + ], + ]); + } + + public function testBug4906(): void + { + $this->analyse([__DIR__ . '/data/bug-4906.php'], []); + } + + public function testBug4910(): void + { + $this->analyse([__DIR__ . '/data/bug-4910.php'], []); + } + + public function testBug3703(): void + { + $this->analyse([__DIR__ . '/data/bug-3703.php'], [ + [ + 'Property Bug3703\Foo::$bar (array>>) does not accept array>>.', + 15, + ], + [ + 'Property Bug3703\Foo::$bar (array>>) does not accept array|int>>.', + 18, + ], + [ + 'Property Bug3703\Foo::$bar (array>>) does not accept array>|string>.', + 21, + ], + ]); + } + + public function testBug6333(): void + { + if (PHP_VERSION_ID < 70400 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->analyse([__DIR__ . '/data/bug-6333.php'], []); + } + + public function testBug3339(): void + { + $this->analyse([__DIR__ . '/data/bug-3339.php'], []); + } + + public function testBug6117(): void + { + $this->analyse([__DIR__ . '/data/bug-6117.php'], []); + } + + public function testGenericObjectWithUnspecifiedTemplateTypes(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/generic-object-unspecified-template-types.php'], [ + [ + 'Property GenericObjectUnspecifiedTemplateTypes\Foo::$obj (ArrayObject) does not accept ArrayObject<(int|string), mixed>.', + 13, + ], + [ + 'Property GenericObjectUnspecifiedTemplateTypes\Bar::$ints (GenericObjectUnspecifiedTemplateTypes\ArrayCollection) does not accept GenericObjectUnspecifiedTemplateTypes\ArrayCollection.', + 67, + ], + ]); + } + + public function testGenericObjectWithUnspecifiedTemplateTypesLevel8(): void + { + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/generic-object-unspecified-template-types.php'], [ + [ + 'Property GenericObjectUnspecifiedTemplateTypes\Bar::$ints (GenericObjectUnspecifiedTemplateTypes\ArrayCollection) does not accept GenericObjectUnspecifiedTemplateTypes\ArrayCollection.', + 67, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php b/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php index b4b5937bfa..901b14816a 100644 --- a/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php +++ b/tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php @@ -38,7 +38,7 @@ public function isInitialized(PropertyReflection $property, string $propertyName ]), [ 'UninitializedProperty\\TestCase::setUp', - ] + ], ); } @@ -80,5 +80,14 @@ public function testPromotedProperties(): void $this->analyse([__DIR__ . '/data/uninitialized-property-promoted.php'], []); } + public function testReadOnly(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1'); + } + + // reported by a different rule + $this->analyse([__DIR__ . '/data/uninitialized-property-readonly.php'], []); + } } diff --git a/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php index c5415f6c57..83f1a922a2 100644 --- a/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/WritingToReadOnlyPropertiesRuleTest.php @@ -2,18 +2,19 @@ namespace PHPStan\Rules\Properties; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class WritingToReadOnlyPropertiesRuleTest extends \PHPStan\Testing\RuleTestCase +class WritingToReadOnlyPropertiesRuleTest extends RuleTestCase { - /** @var bool */ - private $checkThisOnly; + private bool $checkThisOnly; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new WritingToReadOnlyPropertiesRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false), new PropertyDescriptor(), new PropertyReflectionFinder(), $this->checkThisOnly); } diff --git a/tests/PHPStan/Rules/Properties/data/access-private-property-static.php b/tests/PHPStan/Rules/Properties/data/access-private-property-static.php new file mode 100644 index 0000000000..51e6fc3917 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/access-private-property-static.php @@ -0,0 +1,33 @@ += 7.4 + +namespace TestAccessProperties; + +class AssignOpNonexistentProperty +{ + + public function doFoo() + { + $this->flags |= 1; + } + + public function doBar() + { + $this->flags ??= 2; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/access-properties-assign.php b/tests/PHPStan/Rules/Properties/data/access-properties-assign.php index 36c26da329..89f58619b5 100644 --- a/tests/PHPStan/Rules/Properties/data/access-properties-assign.php +++ b/tests/PHPStan/Rules/Properties/data/access-properties-assign.php @@ -7,7 +7,7 @@ class AccessPropertyWithDimFetch public function doFoo() { - $this->foo['foo'] = 'test'; // already reported by a separate rule + $this->foo['foo'] = 'test'; } public function doBar() diff --git a/tests/PHPStan/Rules/Properties/data/access-static-properties-assign-op.php b/tests/PHPStan/Rules/Properties/data/access-static-properties-assign-op.php new file mode 100644 index 0000000000..0cf096c2e1 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/access-static-properties-assign-op.php @@ -0,0 +1,18 @@ += 7.4 + +namespace AccessStaticProperties; + +class AssignOpNonexistentProperty +{ + + public function doFoo() + { + self::$flags |= 1; + } + + public function doBar() + { + self::$flags ??= 2; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/access-static-properties-assign.php b/tests/PHPStan/Rules/Properties/data/access-static-properties-assign.php index a3a8f1b652..869280806c 100644 --- a/tests/PHPStan/Rules/Properties/data/access-static-properties-assign.php +++ b/tests/PHPStan/Rules/Properties/data/access-static-properties-assign.php @@ -7,7 +7,7 @@ class AccessStaticPropertyWithDimFetch public function doFoo() { - self::$foo['foo'] = 'test'; // already reported by a separate rule + self::$foo['foo'] = 'test'; } public function doBar() diff --git a/tests/PHPStan/Rules/Properties/data/access-static-properties.php b/tests/PHPStan/Rules/Properties/data/access-static-properties.php index 84abb1bceb..ebd85a5ba8 100644 --- a/tests/PHPStan/Rules/Properties/data/access-static-properties.php +++ b/tests/PHPStan/Rules/Properties/data/access-static-properties.php @@ -210,3 +210,18 @@ public function doBar(TraitWithStaticProperty $a): void } } + +class AccessWithStatic +{ + + private static $foo; + private $bar; + + public function doBar() + { + static::$foo; // reported by different rule + static::$bar; + static::$nonexistent; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-3339.php b/tests/PHPStan/Rules/Properties/data/bug-3339.php new file mode 100644 index 0000000000..a05f4451b4 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-3339.php @@ -0,0 +1,19 @@ +tuple = [true, true, true]; + + for ($i = 0; $i < 3; ++$i) + { + $this->tuple[$i] = false; + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-3703.php b/tests/PHPStan/Rules/Properties/data/bug-3703.php new file mode 100644 index 0000000000..dd60a9382b --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-3703.php @@ -0,0 +1,24 @@ +> + */ + public $bar; + + public function doFoo() + { + $foo = new self(); + // Should not be allowed (missing string key) + $foo->bar['foo']['bar'][] = 'ok'; + + // Should not be allowed (value should be array) + $foo->bar['foo']['bar'] = 1; + + // Should not be allowed + $foo->bar['ok'] = 'ok'; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-4906.php b/tests/PHPStan/Rules/Properties/data/bug-4906.php new file mode 100644 index 0000000000..ecec635fd3 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-4906.php @@ -0,0 +1,32 @@ + + * @phpstan-var array + * @psalm-var Params + */ + private $params; +} + +class HelloWorld +{ + /** + * @var array + */ + private $connectionParameters; + + private function overrideConnectionParameters(): void + { + $overrideConnectionParameters = \Closure::bind(function (array $connectionParameters) { + foreach ($connectionParameters as $parameterKey => $parameterValue) { + $this->params[$parameterKey] = $parameterValue; + } + }, $this, Connection::class); + $overrideConnectionParameters($this->connectionParameters); + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-4910.php b/tests/PHPStan/Rules/Properties/data/bug-4910.php new file mode 100644 index 0000000000..62da7af9f4 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-4910.php @@ -0,0 +1,39 @@ + + */ + protected $faces = []; + + /** + * @param int[] $faces + * @phpstan-param list $faces + * @return $this + */ + public function setFaces(array $faces) : self{ + $uniqueFaces = []; + foreach($faces as $face){ + if($face !== Facing::NORTH && $face !== Facing::SOUTH && $face !== Facing::WEST && $face !== Facing::EAST){ + throw new \InvalidArgumentException("Facing can only be north, east, south or west"); + } + $uniqueFaces[$face] = $face; + } + + $this->faces = $uniqueFaces; + return $this; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-5607.php b/tests/PHPStan/Rules/Properties/data/bug-5607.php new file mode 100644 index 0000000000..eff2724216 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-5607.php @@ -0,0 +1,19 @@ + 'basic segment']; + + /** + * @param array $x + */ + public function mm($x): void + { + throw new \Exception(); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-5804.php b/tests/PHPStan/Rules/Properties/data/bug-5804.php new file mode 100644 index 0000000000..ba24345e76 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-5804.php @@ -0,0 +1,19 @@ +value[] = 'hello'; + } + + public function doBar() + { + $this->value[] = new Blah; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-5868.php b/tests/PHPStan/Rules/Properties/data/bug-5868.php new file mode 100644 index 0000000000..4359f8b5bf --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-5868.php @@ -0,0 +1,37 @@ += 8.0 + +namespace Bug5868PropertyFetch; + +class Child +{ + + public ?self $child; + + public self $existingChild; + +} + +class Foo +{ + public ?Child $child; +} + +class HelloWorld +{ + + function getAttributeInNode(?Foo $node): ?Child + { + // Ok + $tmp = $node?->child; + $tmp = $node?->child?->child?->child; + $tmp = $node?->child?->existingChild->child; + $tmp = $node?->child?->existingChild->child?->existingChild; + + // Errors + $tmp = $node->child; + $tmp = $node?->child->child; + $tmp = $node?->child->existingChild->child; + $tmp = $node?->child?->existingChild->child->existingChild; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-6020.php b/tests/PHPStan/Rules/Properties/data/bug-6020.php new file mode 100644 index 0000000000..183fd9303f --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-6020.php @@ -0,0 +1,9 @@ += 8.0 + +namespace Bug6020; + +function (): void { + $xml = new \SimpleXMLElement('Whatever'); + + $xml->foo?->bar?->baz; +}; diff --git a/tests/PHPStan/Rules/Properties/data/bug-6117.php b/tests/PHPStan/Rules/Properties/data/bug-6117.php new file mode 100644 index 0000000000..7114703687 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-6117.php @@ -0,0 +1,32 @@ + + */ + private $mappings = []; + + public function testMe(): void + { + $this->mappings[self::CATEGORY_TYPE_TWO] = new Mapping(); + + $this->mappings[(string)self::CATEGORY_TYPE_TWO] = new Mapping(); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-6286.php b/tests/PHPStan/Rules/Properties/data/bug-6286.php new file mode 100644 index 0000000000..d349fb17d6 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-6286.php @@ -0,0 +1,24 @@ += 7.4 + +namespace Bug6286; + +class HelloWorld +{ + /** + * @var array{name:string,age:int} + */ + public array $details; + /** + * @var array + */ + public array $nestedDetails; + + public function doSomething(): void + { + $this->details ['name'] = 'Douglas Adams'; + $this->details ['age'] = 'Forty-two'; + + $this->nestedDetails [0] ['name'] = 'Bilbo Baggins'; + $this->nestedDetails [0] ['age'] = 'Eleventy-one'; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-6333.php b/tests/PHPStan/Rules/Properties/data/bug-6333.php new file mode 100644 index 0000000000..4f478d1195 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-6333.php @@ -0,0 +1,16 @@ += 7.4 + +namespace Bug6333; + +class HelloWorld +{ + /** + * @var array + */ + public array $detectedCheat = []; + + public function test(): void + { + $this->detectedCheat["playerName"][1]++; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-6385.php b/tests/PHPStan/Rules/Properties/data/bug-6385.php new file mode 100644 index 0000000000..330d3fc780 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-6385.php @@ -0,0 +1,53 @@ += 8.1 + +namespace Bug6385; + +use BackedEnum; +use UnitEnum; + +final class EnumValue +{ + public readonly string $name; + public readonly string $value; + + public function __construct( + BackedEnum | string $name, + BackedEnum | string $value + ) { + $this->name = $name instanceof BackedEnum ? $name->name : $name; + $this->value = $value instanceof BackedEnum ? $value->name : $value; + } +} + +enum ActualUnitEnum +{ + +} + +enum ActualBackedEnum: int +{ + +} + +class Foo +{ + + public function doFoo( + UnitEnum $unitEnum, + BackedEnum $backedEnum, + ActualUnitEnum $actualUnitEnum, + ActualBackedEnum $actualBackedEnum + ) + { + echo $unitEnum->name; + echo $unitEnum->value; + echo $backedEnum->name; + echo $backedEnum->value; + echo $actualUnitEnum->name; + echo $actualUnitEnum->value; + echo $actualBackedEnum->name; + echo $actualBackedEnum->value; + + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-6566.php b/tests/PHPStan/Rules/Properties/data/bug-6566.php new file mode 100644 index 0000000000..f592ec686e --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-6566.php @@ -0,0 +1,34 @@ += 8.0 + +namespace Bug6566; + +class A { + public string $name; +} + +class B { + public string $name; +} + +class C { + +} + +/** + * @template T of A|B|C + */ +abstract class HelloWorld +{ + public function sayHelloBug(): void + { + $object = $this->getObject(); + if (!$object instanceof C) { + echo $object->name; + } + } + + /** + * @return T + */ + abstract protected function getObject(): A|B|C; +} diff --git a/tests/PHPStan/Rules/Properties/data/generic-object-unspecified-template-types.php b/tests/PHPStan/Rules/Properties/data/generic-object-unspecified-template-types.php new file mode 100644 index 0000000000..89dff7836d --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/generic-object-unspecified-template-types.php @@ -0,0 +1,93 @@ + */ + private $obj; + + public function __construct() + { + $this->obj = new \ArrayObject(); + } + +} + +/** + * @template TKey of array-key + * @template T + */ +class ArrayCollection +{ + + /** + * @param array $items + */ + public function __construct(array $items = []) + { + + } + +} + +/** + * @template TKey of array-key + * @template T + */ +class ArrayCollection2 +{ + + public function __construct(array $items = []) + { + + } + +} + +class Bar +{ + + /** @var ArrayCollection */ + private $ints; + + public function __construct() + { + $this->ints = new ArrayCollection(); + } + + public function doFoo() + { + $this->ints = new ArrayCollection([]); + } + + public function doBar() + { + $this->ints = new ArrayCollection(['foo', 'bar']); + } + +} + +class Baz +{ + + /** @var ArrayCollection2 */ + private $ints; + + public function __construct() + { + $this->ints = new ArrayCollection2(); + } + + public function doFoo() + { + $this->ints = new ArrayCollection2([]); + } + + public function doBar() + { + $this->ints = new ArrayCollection2(['foo', 'bar']); + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/intersection-types.php b/tests/PHPStan/Rules/Properties/data/intersection-types.php new file mode 100644 index 0000000000..715551fbcf --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/intersection-types.php @@ -0,0 +1,34 @@ += 8.1 + +namespace MissingReadOnlyPropertyAssign; + +class Foo +{ + + private readonly int $assigned; + + private int $unassignedButNotReadOnly; + + private int $readBeforeAssignedNotReadOnly; + + private readonly int $unassigned; + + private readonly int $unassigned2; + + private readonly int $readBeforeAssigned; + + private readonly int $doubleAssigned; + + private int $doubleAssignedNotReadOnly; + + public function __construct( + private readonly int $promoted, + ) + { + $this->assigned = 1; + + echo $this->readBeforeAssignedNotReadOnly; + $this->readBeforeAssignedNotReadOnly = 1; + + echo $this->readBeforeAssigned; + $this->readBeforeAssigned = 1; + + $this->doubleAssigned = 1; + $this->doubleAssigned = 2; + + $this->doubleAssignedNotReadOnly = 1; + $this->doubleAssignedNotReadOnly = 2; + } + + public function setUnassigned2(int $i): void + { + $this->unassigned2 = $i; + } + +} + +class BarDoubleAssignInSetter +{ + + private readonly int $foo; + + public function setFoo(int $i) + { + // reported in ReadOnlyPropertyAssignRule + $this->foo = $i; + $this->foo = $i; + } + +} + +class TestCase +{ + + private readonly int $foo; + + protected function setUp(): void + { + $this->foo = 1; + } + +} + +class AssignOp +{ + + private readonly int $foo; + + private readonly ?int $bar; + + public function __construct(int $foo) + { + $this->foo .= $foo; + + $this->bar ??= 3; + } + + +} + +class AssignRef +{ + + private readonly int $foo; + + public function __construct(int $foo) + { + $this->foo = &$foo; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/overriding-property-phpdoc.php b/tests/PHPStan/Rules/Properties/data/overriding-property-phpdoc.php new file mode 100644 index 0000000000..6e6a7d0f2e --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/overriding-property-phpdoc.php @@ -0,0 +1,31 @@ + */ + protected $arrayClassStrings; + + /** @var string */ + protected $string; + +} + +class Bar extends Foo +{ + + /** @var array */ + protected $array; + + /** @var array */ + protected $arrayClassStrings; + + /** @var int */ + protected $string; + +} diff --git a/tests/PHPStan/Rules/Properties/data/overriding-property.php b/tests/PHPStan/Rules/Properties/data/overriding-property.php new file mode 100644 index 0000000000..cf0b46cd4c --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/overriding-property.php @@ -0,0 +1,160 @@ + */ + private $foo; + + /** @var int<3, max> */ + private $bar; + + public function doFoo(): void + { + $this->foo--; + $this->bar++; + } + + public function doBar(): void + { + $this->foo++; + $this->bar--; + } + + public function doFoo2(): void + { + --$this->foo; + ++$this->bar; + } + + public function doBar2(): void + { + ++$this->foo; + --$this->bar; + } + +} + +class ListAssign +{ + + /** @var string */ + private $foo; + + public function doFoo() + { + [$this->foo] = [1]; + } + +} + +class AppendToArrayAccess +{ + /** @var \ArrayAccess */ + private $collection1; + + /** @var \ArrayAccess&\Countable */ + private $collection2; + + public function foo(): void + { + $this->collection1[] = 1; + $this->collection2[] = 2; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/read-only-property.php b/tests/PHPStan/Rules/Properties/data/read-only-property.php new file mode 100644 index 0000000000..979cf94e91 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/read-only-property.php @@ -0,0 +1,19 @@ += 8.1 + +namespace ReadOnlyPropertyAssignRef; + +class Foo +{ + + private readonly int $foo; + + public readonly int $bar; + + public function doFoo() + { + $foo = &$this->foo; + $bar = &$this->bar; + } + +} + +class Bar +{ + + public function doBar(Foo $foo) + { + $a = &$foo->foo; // private + $b = &$foo->bar; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/readonly-assign.php b/tests/PHPStan/Rules/Properties/data/readonly-assign.php new file mode 100644 index 0000000000..cfead51c27 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/readonly-assign.php @@ -0,0 +1,186 @@ += 8.1 + +namespace ReadonlyPropertyAssign; + +class Foo +{ + + private readonly int $foo; + + protected readonly int $bar; + + public readonly int $baz; + + public function __construct(int $foo) + { + $this->foo = $foo; // constructor - fine + } + + public function setFoo(int $foo): void + { + $this->foo = $foo; // setter - report + } + +} + +class Bar extends Foo +{ + + public function __construct(int $bar) + { + parent::__construct(1); + $this->foo = $foo; // do not report - private property + $this->bar = $bar; // report - not in declaring class + $this->baz = $baz; // report - not in declaring class + } + + public function setBar(int $bar): void + { + $this->bar = $bar; // report - not in declaring class + } + +} + +function (Foo $foo): void { + $foo->foo = 1; // do not report - private property + $foo->baz = 2; // report - not in declaring class +}; + +class FooArrays +{ + + /** + * @var array{name:string,age:int} + */ + public readonly array $details; + + public function __construct() + { + $this->details = ['name' => 'Foo', 'age' => 25]; + } + + public function doSomething(): void + { + $this->details['name'] = 'Bob'; + $this->details['age'] = 42; + } + +} + +class NotReadonly +{ + + private int $foo; + + public function setFoo(int $foo): void + { + $this->foo = $foo; // do not report - not readonly + } + +} + +class NotThis +{ + + private readonly int $foo; + + public function __construct(int $foo) + { + $self = new self(1); + $self->foo = $foo; // report - not $this + } + +} + +class PostInc +{ + + private readonly int $foo; + + public function doFoo(): void + { + $this->foo++; + --$this->foo; + + $this->foo += 5; + } + +} + +class ListAssign +{ + + private readonly int $foo; + + public function __construct() + { + [$this->foo] = [1]; + } + + public function setFoo() + { + [$this->foo] = [1]; + } + + public function setBar() + { + list($this->foo) = [1]; + } + +} + +enum FooEnum: string +{ + + case ONE = 'one'; + case TWO = 'two'; + + public function doFoo(): void + { + $this->name = 'ONE'; + $this->value = 'one'; + } + +} + +class TestFooEnum +{ + + public function doFoo(FooEnum $foo): void + { + $foo->name = 'ONE'; + $foo->value = 'one'; + } + +} + +class AssignRefOutsideClass +{ + + public function doFoo(Foo $foo, int $i) + { + $foo->baz = 5; + $foo->baz = &$i; + } + +} + +class Unserialization +{ + + private readonly int $foo; + + public function __construct(int $foo) + { + $this->foo = $foo; // constructor - fine + } + + /** + * @param array $data + */ + public function __unserialize(array $data) : void + { + [$this->foo] = $data; // __unserialize - fine + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/uninitialized-property-readonly.php b/tests/PHPStan/Rules/Properties/data/uninitialized-property-readonly.php new file mode 100644 index 0000000000..c47d6e765d --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/uninitialized-property-readonly.php @@ -0,0 +1,28 @@ += 8.1 + +namespace UninitializedPropertyReadonly; + +class Foo +{ + + private readonly int $bar; + + public function __construct() + { + + } + +} + +class Bar +{ + + private readonly int $bar; + + public function __construct() + { + echo $this->bar; + $this->bar = 1; + } + +} diff --git a/tests/PHPStan/Rules/Properties/data/uninitialized-property.php b/tests/PHPStan/Rules/Properties/data/uninitialized-property.php index b2b7406014..671ed79c8a 100644 --- a/tests/PHPStan/Rules/Properties/data/uninitialized-property.php +++ b/tests/PHPStan/Rules/Properties/data/uninitialized-property.php @@ -122,3 +122,29 @@ class TestExtension private int $uninited; } + +class ImplicitArrayCreation +{ + + /** @var mixed[] */ + private array $properties; + + private function __construct(string $message) + { + $this->properties['message'] = $message; + } + +} + +class ImplicitArrayCreation2 +{ + + /** @var mixed[] */ + private array $properties; + + private function __construct(string $message) + { + $this->properties['foo']['message'] = $message; + } + +} diff --git a/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php b/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php index 3c53137056..4c17fc2f0c 100644 --- a/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php +++ b/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php @@ -2,13 +2,17 @@ namespace PHPStan\Rules\Regexp; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class RegularExpressionPatternRuleTest extends \PHPStan\Testing\RuleTestCase +class RegularExpressionPatternRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new RegularExpressionPatternRule(); } @@ -110,7 +114,7 @@ public function testValidRegexPatternBefore73(): void 'Regex pattern is invalid: Compilation failed: missing ) at offset 1 in pattern: ~(~', 43, ], - ] + ], ); } @@ -211,7 +215,7 @@ public function testValidRegexPatternAfter73(): void 'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~', 43, ], - ] + ], ); } diff --git a/tests/PHPStan/Rules/RegistryTest.php b/tests/PHPStan/Rules/RegistryTest.php index 77469e4362..cd9aeb6c5b 100644 --- a/tests/PHPStan/Rules/RegistryTest.php +++ b/tests/PHPStan/Rules/RegistryTest.php @@ -2,9 +2,11 @@ namespace PHPStan\Rules; +use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Testing\PHPStanTestCase; -class RegistryTest extends \PHPStan\Testing\TestCase +class RegistryTest extends PHPStanTestCase { public function testGetRules(): void @@ -15,33 +17,29 @@ public function testGetRules(): void $rule, ]); - $rules = $registry->getRules(\PhpParser\Node\Expr\FuncCall::class); + $rules = $registry->getRules(Node\Expr\FuncCall::class); $this->assertCount(1, $rules); $this->assertSame($rule, $rules[0]); - $this->assertCount(0, $registry->getRules(\PhpParser\Node\Expr\MethodCall::class)); + $this->assertCount(0, $registry->getRules(Node\Expr\MethodCall::class)); } public function testGetRulesWithTwoDifferentInstances(): void { - $fooRule = new UniversalRule(\PhpParser\Node\Expr\FuncCall::class, static function (\PhpParser\Node\Expr\FuncCall $node, Scope $scope): array { - return ['Foo error']; - }); - $barRule = new UniversalRule(\PhpParser\Node\Expr\FuncCall::class, static function (\PhpParser\Node\Expr\FuncCall $node, Scope $scope): array { - return ['Bar error']; - }); + $fooRule = new UniversalRule(Node\Expr\FuncCall::class, static fn (Node\Expr\FuncCall $node, Scope $scope): array => ['Foo error']); + $barRule = new UniversalRule(Node\Expr\FuncCall::class, static fn (Node\Expr\FuncCall $node, Scope $scope): array => ['Bar error']); $registry = new Registry([ $fooRule, $barRule, ]); - $rules = $registry->getRules(\PhpParser\Node\Expr\FuncCall::class); + $rules = $registry->getRules(Node\Expr\FuncCall::class); $this->assertCount(2, $rules); $this->assertSame($fooRule, $rules[0]); $this->assertSame($barRule, $rules[1]); - $this->assertCount(0, $registry->getRules(\PhpParser\Node\Expr\MethodCall::class)); + $this->assertCount(0, $registry->getRules(Node\Expr\MethodCall::class)); } } diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRuleTest.php index 00b517040d..cf82486a1e 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideArrowFunctionReturnTypehintRuleTest.php @@ -4,9 +4,10 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class TooWideArrowFunctionReturnTypehintRuleTest extends RuleTestCase { @@ -23,7 +24,7 @@ public function testRule(): void } $this->analyse([__DIR__ . '/data/tooWideArrowFunctionReturnType.php'], [ [ - 'Anonymous function never returns null so it can be removed from the return typehint.', + 'Anonymous function never returns null so it can be removed from the return type.', 14, ], ]); diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideClosureReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideClosureReturnTypehintRuleTest.php index 13e17f6aa6..f47472a2dd 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideClosureReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideClosureReturnTypehintRuleTest.php @@ -6,7 +6,7 @@ use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class TooWideClosureReturnTypehintRuleTest extends RuleTestCase { @@ -20,7 +20,7 @@ public function testRule(): void { $this->analyse([__DIR__ . '/data/tooWideClosureReturnType.php'], [ [ - 'Anonymous function never returns null so it can be removed from the return typehint.', + 'Anonymous function never returns null so it can be removed from the return type.', 20, ], ]); diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php index 916cc3cefd..8b6438796d 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php @@ -6,7 +6,7 @@ use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class TooWideFunctionReturnTypehintRuleTest extends RuleTestCase { @@ -21,17 +21,29 @@ public function testRule(): void require_once __DIR__ . '/data/tooWideFunctionReturnType.php'; $this->analyse([__DIR__ . '/data/tooWideFunctionReturnType.php'], [ [ - 'Function TooWideFunctionReturnType\bar() never returns string so it can be removed from the return typehint.', + 'Function TooWideFunctionReturnType\bar() never returns string so it can be removed from the return type.', 11, ], [ - 'Function TooWideFunctionReturnType\baz() never returns null so it can be removed from the return typehint.', + 'Function TooWideFunctionReturnType\baz() never returns null so it can be removed from the return type.', 15, ], [ - 'Function TooWideFunctionReturnType\ipsum() never returns null so it can be removed from the return typehint.', + 'Function TooWideFunctionReturnType\ipsum() never returns null so it can be removed from the return type.', 27, ], + [ + 'Function TooWideFunctionReturnType\dolor2() never returns null so it can be removed from the return type.', + 41, + ], + [ + 'Function TooWideFunctionReturnType\dolor4() never returns int so it can be removed from the return type.', + 59, + ], + [ + 'Function TooWideFunctionReturnType\dolor6() never returns null so it can be removed from the return type.', + 79, + ], ]); } diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php index 6ed2a9cf44..0f3ff533a1 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php @@ -4,9 +4,10 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ class TooWideMethodReturnTypehintRuleTest extends RuleTestCase { @@ -20,17 +21,29 @@ public function testPrivate(): void { $this->analyse([__DIR__ . '/data/tooWideMethodReturnType-private.php'], [ [ - 'Method TooWideMethodReturnType\Foo::bar() never returns string so it can be removed from the return typehint.', + 'Method TooWideMethodReturnType\Foo::bar() never returns string so it can be removed from the return type.', 14, ], [ - 'Method TooWideMethodReturnType\Foo::baz() never returns null so it can be removed from the return typehint.', + 'Method TooWideMethodReturnType\Foo::baz() never returns null so it can be removed from the return type.', 18, ], [ - 'Method TooWideMethodReturnType\Foo::dolor() never returns null so it can be removed from the return typehint.', + 'Method TooWideMethodReturnType\Foo::dolor() never returns null so it can be removed from the return type.', 34, ], + [ + 'Method TooWideMethodReturnType\Foo::dolor2() never returns null so it can be removed from the return type.', + 48, + ], + [ + 'Method TooWideMethodReturnType\Foo::dolor4() never returns int so it can be removed from the return type.', + 66, + ], + [ + 'Method TooWideMethodReturnType\Foo::dolor6() never returns null so it can be removed from the return type.', + 86, + ], ]); } @@ -38,15 +51,15 @@ public function testPublicProtected(): void { $this->analyse([__DIR__ . '/data/tooWideMethodReturnType-public-protected.php'], [ [ - 'Method TooWideMethodReturnType\Bar::bar() never returns string so it can be removed from the return typehint.', + 'Method TooWideMethodReturnType\Bar::bar() never returns string so it can be removed from the return type.', 14, ], [ - 'Method TooWideMethodReturnType\Bar::baz() never returns null so it can be removed from the return typehint.', + 'Method TooWideMethodReturnType\Bar::baz() never returns null so it can be removed from the return type.', 18, ], [ - 'Method TooWideMethodReturnType\Bazz::lorem() never returns string so it can be removed from the return typehint.', + 'Method TooWideMethodReturnType\Bazz::lorem() never returns string so it can be removed from the return type.', 35, ], ]); @@ -56,11 +69,11 @@ public function testPublicProtectedWithInheritance(): void { $this->analyse([__DIR__ . '/data/tooWideMethodReturnType-public-protected-inheritance.php'], [ [ - 'Method TooWideMethodReturnType\Baz::baz() never returns null so it can be removed from the return typehint.', + 'Method TooWideMethodReturnType\Baz::baz() never returns null so it can be removed from the return type.', 27, ], [ - 'Method TooWideMethodReturnType\BarClass::doFoo() never returns null so it can be removed from the return typehint.', + 'Method TooWideMethodReturnType\BarClass::doFoo() never returns null so it can be removed from the return type.', 51, ], ]); @@ -70,10 +83,18 @@ public function testBug5095(): void { $this->analyse([__DIR__ . '/data/bug-5095.php'], [ [ - 'Method Bug5095\Parser::unaryOperatorFor() never returns \'not\' so it can be removed from the return typehint.', + 'Method Bug5095\Parser::unaryOperatorFor() never returns \'not\' so it can be removed from the return type.', 21, ], ]); } + public function testBug6158(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + $this->analyse([__DIR__ . '/data/bug-6158.php'], []); + } + } diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-6158.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-6158.php new file mode 100644 index 0000000000..3dd1798fc2 --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-6158.php @@ -0,0 +1,87 @@ += 7.4 + +namespace Bug6158; + +/** + * @template TKey of array-key + * @template T + * @implements ArrayAccess + */ +class Collection implements \ArrayAccess { + + /** @var array */ + private array $values; + + /** + * @param TKey $offset + */ + final public function offsetExists(mixed $offset): bool + { + return array_key_exists($offset, $this->values); + } + + /** + * @param TKey $offset + * + * @return T + */ + final public function offsetGet(mixed $offset): mixed + { + return $this->values[$offset]; + } + + /** + * @param TKey|null $offset + * @param T $value + */ + final public function offsetSet($offset, $value): void + { + $this->values[$offset] = $value; + } + + /** + * @param TKey $offset + */ + final public function offsetUnset($offset): void + { + unset($this->values[$offset]); + } + + /** @return T|null */ + final public function randValue(): mixed + { + if ($this->values === []) { + return null; + } + + return $this[array_rand($this->values)]; + } +} + +final class User { + + public UserCollection $users; + + public function __construct() + { + $this->users = new UserCollection(); + } + + public function randValue(): ?User + { + return $this->rand(); + } + + private function rand(): ?User + { + return $this->users->randValue(); + } + +} + +/** + * @extends Collection + */ +class UserCollection extends Collection +{ +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/tooWideFunctionReturnType.php b/tests/PHPStan/Rules/TooWideTypehints/data/tooWideFunctionReturnType.php index 513870cff7..8c55f35252 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/data/tooWideFunctionReturnType.php +++ b/tests/PHPStan/Rules/TooWideTypehints/data/tooWideFunctionReturnType.php @@ -37,3 +37,49 @@ public function doFoo() { return 'str'; } + +function dolor2(): ?string { + if (rand()) { + return 'foo'; + } +} + +/** + * @return string|null + */ +function dolor3() { + if (rand()) { + return 'foo'; + } +} + +/** + * @return string|int + */ +function dolor4() { + if (rand()) { + return 'foo'; + } +} + +/** + * @return string|null + */ +function dolor5() { + if (rand()) { + return 'foo'; + } + + return null; +} + +/** + * @return string|null + */ +function dolor6() { + if (rand()) { + return 'foo'; + } + + return 'bar'; +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/tooWideMethodReturnType-private.php b/tests/PHPStan/Rules/TooWideTypehints/data/tooWideMethodReturnType-private.php index 475b2319a5..aa3f6259a6 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/data/tooWideMethodReturnType-private.php +++ b/tests/PHPStan/Rules/TooWideTypehints/data/tooWideMethodReturnType-private.php @@ -45,4 +45,50 @@ public function doFoo() { return 'str'; } + private function dolor2(): ?string { + if (rand()) { + return 'foo'; + } + } + + /** + * @return string|null + */ + private function dolor3() { + if (rand()) { + return 'foo'; + } + } + + /** + * @return string|int + */ + private function dolor4() { + if (rand()) { + return 'foo'; + } + } + + /** + * @return string|null + */ + private function dolor5() { + if (rand()) { + return 'foo'; + } + + return null; + } + + /** + * @return string|null + */ + private function dolor6() { + if (rand()) { + return 'foo'; + } + + return 'bar'; + } + } diff --git a/tests/PHPStan/Rules/UniversalRule.php b/tests/PHPStan/Rules/UniversalRule.php index 7d4f225c45..f63ca4d30b 100644 --- a/tests/PHPStan/Rules/UniversalRule.php +++ b/tests/PHPStan/Rules/UniversalRule.php @@ -6,7 +6,7 @@ use PHPStan\Analyser\Scope; /** - * @template TNodeType of \PhpParser\Node + * @template TNodeType of Node * @implements Rule */ class UniversalRule implements Rule @@ -35,7 +35,6 @@ public function getNodeType(): string /** * @param TNodeType $node - * @param \PHPStan\Analyser\Scope $scope * @return array */ public function processNode(Node $node, Scope $scope): array diff --git a/tests/PHPStan/Rules/Variables/CompactVariablesRuleTest.php b/tests/PHPStan/Rules/Variables/CompactVariablesRuleTest.php index 20dfb5bc4d..ccd8f350fc 100644 --- a/tests/PHPStan/Rules/Variables/CompactVariablesRuleTest.php +++ b/tests/PHPStan/Rules/Variables/CompactVariablesRuleTest.php @@ -3,15 +3,15 @@ namespace PHPStan\Rules\Variables; use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class CompactVariablesRuleTest extends \PHPStan\Testing\RuleTestCase +class CompactVariablesRuleTest extends RuleTestCase { - /** @var bool */ - private $checkMaybeUndefinedVariables; + private bool $checkMaybeUndefinedVariables; protected function getRule(): Rule { diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index c481973ded..6349d31808 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -2,32 +2,29 @@ namespace PHPStan\Rules\Variables; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class DefinedVariableRuleTest extends \PHPStan\Testing\RuleTestCase +class DefinedVariableRuleTest extends RuleTestCase { - /** @var bool */ - private $cliArgumentsVariablesRegistered; - - /** @var bool */ - private $checkMaybeUndefinedVariables; + private bool $cliArgumentsVariablesRegistered; - /** @var bool */ - private $polluteScopeWithLoopInitialAssignments; + private bool $checkMaybeUndefinedVariables; - /** @var bool */ - private $polluteCatchScopeWithTryAssignments; + private bool $polluteScopeWithLoopInitialAssignments; - /** @var bool */ - private $polluteScopeWithAlwaysIterableForeach; + private bool $polluteScopeWithAlwaysIterableForeach; - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new DefinedVariableRule( $this->cliArgumentsVariablesRegistered, - $this->checkMaybeUndefinedVariables + $this->checkMaybeUndefinedVariables, ); } @@ -36,11 +33,6 @@ protected function shouldPolluteScopeWithLoopInitialAssignments(): bool return $this->polluteScopeWithLoopInitialAssignments; } - protected function shouldPolluteCatchScopeWithTryAssignments(): bool - { - return $this->polluteCatchScopeWithTryAssignments; - } - protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool { return $this->polluteScopeWithAlwaysIterableForeach; @@ -51,7 +43,6 @@ public function testDefinedVariables(): void require_once __DIR__ . '/data/defined-variables-definition.php'; $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/defined-variables.php'], [ @@ -107,10 +98,6 @@ public function testDefinedVariables(): void 'Undefined variable: $variableInEmpty', 145, ], - [ - 'Undefined variable: $negatedVariableInEmpty', - 152, - ], [ 'Undefined variable: $variableInEmpty', 155, @@ -258,7 +245,6 @@ public function testDefinedVariablesInClosures(): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/defined-variables-closures.php'], [ @@ -273,7 +259,6 @@ public function testDefinedVariablesInShortArrayDestructuringSyntax(): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/defined-variables-array-destructuring-short-syntax.php'], [ @@ -296,7 +281,6 @@ public function testCliArgumentsVariablesNotRegistered(): void { $this->cliArgumentsVariablesRegistered = false; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/cli-arguments-variables.php'], [ @@ -315,7 +299,6 @@ public function testCliArgumentsVariablesRegistered(): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/cli-arguments-variables.php'], [ @@ -363,18 +346,15 @@ public function dataLoopInitialAssignments(): array /** * @dataProvider dataLoopInitialAssignments - * @param bool $polluteScopeWithLoopInitialAssignments - * @param bool $checkMaybeUndefinedVariables - * @param mixed[][] $expectedErrors + * @param list $expectedErrors */ public function testLoopInitialAssignments( bool $polluteScopeWithLoopInitialAssignments, bool $checkMaybeUndefinedVariables, - array $expectedErrors + array $expectedErrors, ): void { $this->cliArgumentsVariablesRegistered = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->polluteScopeWithLoopInitialAssignments = $polluteScopeWithLoopInitialAssignments; $this->checkMaybeUndefinedVariables = $checkMaybeUndefinedVariables; $this->polluteScopeWithAlwaysIterableForeach = true; @@ -385,7 +365,6 @@ public function testDefineVariablesInClass(): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/define-variables-class.php'], []); @@ -395,7 +374,6 @@ public function testDeadBranches(): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/dead-branches.php'], [ @@ -426,7 +404,6 @@ public function testForeach(): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/foreach.php'], [ @@ -456,27 +433,27 @@ public function testForeach(): void ], [ 'Undefined variable: $val', - 171, + 200, ], [ 'Undefined variable: $test', - 172, + 201, ], [ 'Undefined variable: $val', - 187, + 216, ], [ 'Undefined variable: $test', - 188, + 217, ], [ 'Variable $val might not be defined.', - 217, + 246, ], [ 'Variable $test might not be defined.', - 218, + 247, ], ]); } @@ -592,14 +569,12 @@ public function dataForeachPolluteScopeWithAlwaysIterableForeach(): array /** * @dataProvider dataForeachPolluteScopeWithAlwaysIterableForeach * - * @param bool $polluteScopeWithAlwaysIterableForeach * @param mixed[] $errors */ public function testForeachPolluteScopeWithAlwaysIterableForeach(bool $polluteScopeWithAlwaysIterableForeach, array $errors): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = $polluteScopeWithAlwaysIterableForeach; $this->analyse([__DIR__ . '/data/foreach-always-iterable.php'], $errors); @@ -609,7 +584,6 @@ public function testBooleanOperatorsTruthyFalsey(): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/boolean-op-truthy-falsey.php'], [ @@ -632,7 +606,6 @@ public function testArrowFunctions(): void $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/defined-variables-arrow-functions.php'], [ @@ -655,7 +628,6 @@ public function testCoalesceAssign(): void $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/defined-variables-coalesce-assign.php'], [ @@ -670,7 +642,6 @@ public function testBug2748(): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-2748.php'], [ @@ -689,7 +660,6 @@ public function testGlobalVariables(): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/global-variables.php'], []); @@ -699,7 +669,6 @@ public function testRootScopeMaybeDefined(): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = false; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/root-scope-maybe.php'], []); @@ -709,7 +678,6 @@ public function testRootScopeMaybeDefinedCheck(): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/root-scope-maybe.php'], [ @@ -728,7 +696,6 @@ public function testFormerThisVariableRule(): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/this.php'], [ @@ -755,7 +722,6 @@ public function testClosureUse(): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/defined-variables-anonymous-function-use.php'], [ @@ -794,7 +760,6 @@ public function testNullsafeIsset(): void $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/variable-nullsafe-isset.php'], []); @@ -804,7 +769,6 @@ public function testBug1306(): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-1306.php'], []); @@ -814,7 +778,6 @@ public function testBug3515(): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-3515.php'], [ @@ -833,7 +796,6 @@ public function testBug4412(): void { $this->cliArgumentsVariablesRegistered = true; $this->polluteScopeWithLoopInitialAssignments = false; - $this->polluteCatchScopeWithTryAssignments = false; $this->checkMaybeUndefinedVariables = true; $this->polluteScopeWithAlwaysIterableForeach = true; $this->analyse([__DIR__ . '/data/bug-4412.php'], [ @@ -844,4 +806,60 @@ public function testBug4412(): void ]); } + public function testBug3283(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-3283.php'], []); + } + + public function testFirstClassCallables(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1.'); + } + + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/first-class-callables.php'], [ + [ + 'Undefined variable: $foo', + 10, + ], + [ + 'Undefined variable: $foo', + 11, + ], + [ + 'Undefined variable: $foo', + 29, + ], + [ + 'Undefined variable: $foo', + 30, + ], + [ + 'Undefined variable: $foo', + 48, + ], + [ + 'Undefined variable: $foo', + 49, + ], + ]); + } + + public function testBug6112(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-6112.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/EmptyRuleTest.php b/tests/PHPStan/Rules/Variables/EmptyRuleTest.php new file mode 100644 index 0000000000..4fcb08548b --- /dev/null +++ b/tests/PHPStan/Rules/Variables/EmptyRuleTest.php @@ -0,0 +1,84 @@ + + */ +class EmptyRuleTest extends RuleTestCase +{ + + private bool $treatPhpDocTypesAsCertain; + + protected function getRule(): Rule + { + return new EmptyRule(new IssetCheck( + new PropertyDescriptor(), + new PropertyReflectionFinder(), + true, + $this->treatPhpDocTypesAsCertain, + )); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return $this->treatPhpDocTypesAsCertain; + } + + public function testRule(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/empty-rule.php'], [ + [ + 'Offset \'nonexistent\' on array{0?: bool, 1?: false, 2: bool, 3: false, 4: true} in empty() does not exist.', + 22, + ], + [ + 'Offset 3 on array{0?: bool, 1?: false, 2: bool, 3: false, 4: true} in empty() always exists and is always falsy.', + 24, + ], + [ + 'Offset 4 on array{0?: bool, 1?: false, 2: bool, 3: false, 4: true} in empty() always exists and is not falsy.', + 25, + ], + [ + 'Offset 0 on array{\'\', \'0\', \'foo\', \'\'|\'foo\'} in empty() always exists and is always falsy.', + 36, + ], + [ + 'Offset 1 on array{\'\', \'0\', \'foo\', \'\'|\'foo\'} in empty() always exists and is always falsy.', + 37, + ], + [ + 'Offset 2 on array{\'\', \'0\', \'foo\', \'\'|\'foo\'} in empty() always exists and is not falsy.', + 38, + ], + [ + 'Variable $a in empty() is never defined.', + 44, + ], + [ + 'Variable $b in empty() always exists and is not falsy.', + 47, + ], + ]); + } + + public function testBug970(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-970.php'], [ + [ + 'Variable $ar in empty() is never defined.', + 9, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Variables/IssetRuleTest.php b/tests/PHPStan/Rules/Variables/IssetRuleTest.php index 7e4ee5150a..1ee394fea7 100644 --- a/tests/PHPStan/Rules/Variables/IssetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/IssetRuleTest.php @@ -7,6 +7,7 @@ use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -14,32 +15,57 @@ class IssetRuleTest extends RuleTestCase { + private bool $treatPhpDocTypesAsCertain; + protected function getRule(): Rule { - return new IssetRule(new IssetCheck(new PropertyDescriptor(), new PropertyReflectionFinder())); + return new IssetRule(new IssetCheck( + new PropertyDescriptor(), + new PropertyReflectionFinder(), + true, + $this->treatPhpDocTypesAsCertain, + )); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return $this->treatPhpDocTypesAsCertain; } public function testRule(): void { + $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/isset.php'], [ [ 'Property IssetRule\FooCoalesce::$string (string) in isset() is not nullable.', 32, ], [ - 'Offset \'string\' on array(1, 2, 3) in isset() does not exist.', + 'Variable $scalar in isset() always exists and is not nullable.', + 41, + ], + [ + 'Offset \'string\' on array{1, 2, 3} in isset() does not exist.', 45, ], [ - 'Offset \'string\' on array(array(1), array(2), array(3)) in isset() does not exist.', + 'Offset \'string\' on array{array{1}, array{2}, array{3}} in isset() does not exist.', 49, ], [ - 'Offset \'dim\' on array(\'dim\' => 1, \'dim-null\' => 1|null, \'dim-null-offset\' => array(\'a\' => true|null), \'dim-empty\' => array()) in isset() always exists and is not nullable.', + 'Variable $doesNotExist in isset() is never defined.', + 51, + ], + [ + 'Offset \'dim\' on array{dim: 1, dim-null: 1|null, dim-null-offset: array{a: true|null}, dim-empty: array{}} in isset() always exists and is not nullable.', 67, ], [ - 'Offset \'b\' on array() in isset() does not exist.', + 'Offset \'dim-null-not-set\' on array{dim: 1, dim-null: 1|null, dim-null-offset: array{a: true|null}, dim-empty: array{}} in isset() does not exist.', + 73, + ], + [ + 'Offset \'b\' on array{} in isset() does not exist.', 79, ], [ @@ -62,6 +88,97 @@ public function testRule(): void 'Static property IssetRule\FooCoalesce::$staticAlwaysNull (null) in isset() is always null.', 97, ], + [ + 'Variable $a in isset() always exists and is always null.', + 111, + ], + [ + 'Property IssetRule\FooCoalesce::$string (string) in isset() is not nullable.', + 116, + ], + [ + 'Property IssetRule\FooCoalesce::$alwaysNull (null) in isset() is always null.', + 118, + ], + [ + 'Static property IssetRule\FooCoalesce::$staticAlwaysNull (null) in isset() is always null.', + 123, + ], + [ + 'Static property IssetRule\FooCoalesce::$staticString (string) in isset() is not nullable.', + 124, + ], + [ + 'Offset \'foo\' on array{foo: string} in isset() always exists and is not nullable.', + 170, + ], + [ + 'Offset \'bar\' on array{bar: 1} in isset() always exists and is not nullable.', + 173, + ], + ]); + } + + public function testRuleWithoutTreatPhpDocTypesAsCertain(): void + { + $this->treatPhpDocTypesAsCertain = false; + $this->analyse([__DIR__ . '/data/isset.php'], [ + [ + 'Property IssetRule\FooCoalesce::$string (string) in isset() is not nullable.', + 32, + ], + [ + 'Variable $scalar in isset() always exists and is not nullable.', + 41, + ], + [ + 'Offset \'string\' on array{1, 2, 3} in isset() does not exist.', + 45, + ], + [ + 'Offset \'string\' on array{array{1}, array{2}, array{3}} in isset() does not exist.', + 49, + ], + [ + 'Variable $doesNotExist in isset() is never defined.', + 51, + ], + [ + 'Offset \'dim\' on array{dim: 1, dim-null: 1|null, dim-null-offset: array{a: true|null}, dim-empty: array{}} in isset() always exists and is not nullable.', + 67, + ], + [ + 'Offset \'dim-null-not-set\' on array{dim: 1, dim-null: 1|null, dim-null-offset: array{a: true|null}, dim-empty: array{}} in isset() does not exist.', + 73, + ], + [ + 'Offset \'b\' on array{} in isset() does not exist.', + 79, + ], + [ + 'Property IssetRule\FooCoalesce::$string (string) in isset() is not nullable.', + 85, + ], + [ + 'Property IssetRule\FooCoalesce::$alwaysNull (null) in isset() is always null.', + 87, + ], + [ + 'Property IssetRule\FooCoalesce::$string (string) in isset() is not nullable.', + 89, + ], + [ + 'Static property IssetRule\FooCoalesce::$staticString (string) in isset() is not nullable.', + 95, + ], + [ + 'Static property IssetRule\FooCoalesce::$staticAlwaysNull (null) in isset() is always null.', + 97, + ], + [ + 'Variable $a in isset() always exists and is always null.', + 111, + ], [ 'Property IssetRule\FooCoalesce::$string (string) in isset() is not nullable.', 116, @@ -86,6 +203,7 @@ public function testNativePropertyTypes(): void if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { $this->markTestSkipped('Test requires PHP 7.4.'); } + $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/isset-native-property-types.php'], [ /*[ // no way to achieve this with current PHP Reflection API @@ -103,15 +221,113 @@ public function testNativePropertyTypes(): void public function testBug4290(): void { + $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-4290.php'], []); } public function testBug4671(): void { + $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/bug-4671.php'], [[ - 'Offset string&numeric on array in isset() does not exist.', + 'Offset numeric-string on array in isset() does not exist.', 13, ]]); } + public function testVariableCertaintyInIsset(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/variable-certainty-isset.php'], [ + [ + 'Variable $alwaysDefinedNotNullable in isset() always exists and is not nullable.', + 14, + ], + [ + 'Variable $neverDefinedVariable in isset() is never defined.', + 22, + ], + [ + 'Variable $anotherNeverDefinedVariable in isset() is never defined.', + 42, + ], + [ + 'Variable $yetAnotherNeverDefinedVariable in isset() is never defined.', + 46, + ], + [ + 'Variable $yetYetAnotherNeverDefinedVariableInIsset in isset() is never defined.', + 56, + ], + [ + 'Variable $anotherVariableInDoWhile in isset() always exists and is not nullable.', + 104, + ], + [ + 'Variable $variableInSecondCase in isset() is never defined.', + 110, + ], + [ + 'Variable $variableInFirstCase in isset() always exists and is not nullable.', + 112, + ], + [ + 'Variable $variableInFirstCase in isset() always exists and is not nullable.', + 116, + ], + [ + 'Variable $variableInSecondCase in isset() always exists and is always null.', + 117, + ], + [ + 'Variable $variableAssignedInSecondCase in isset() is never defined.', + 119, + ], + [ + 'Variable $alwaysDefinedForSwitchCondition in isset() always exists and is not nullable.', + 139, + ], + [ + 'Variable $alwaysDefinedForCaseNodeCondition in isset() always exists and is not nullable.', + 140, + ], + [ + 'Variable $alwaysDefinedNotNullable in isset() always exists and is not nullable.', + 152, + ], + [ + 'Variable $neverDefinedVariable in isset() is never defined.', + 152, + ], + [ + 'Variable $a in isset() always exists and is not nullable.', + 214, + ], + [ + 'Variable $null in isset() always exists and is always null.', + 225, + ], + ]); + } + + public function testIssetInGlobalScope(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/isset-global-scope.php'], [ + [ + 'Variable $alwaysDefinedNotNullable in isset() always exists and is not nullable.', + 8, + ], + ]); + } + + public function testNullsafe(): void + { + if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/isset-nullsafe.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php index 681ee02a24..4bc02691d9 100644 --- a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php +++ b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php @@ -5,39 +5,67 @@ use PHPStan\Rules\IssetCheck; use PHPStan\Rules\Properties\PropertyDescriptor; use PHPStan\Rules\Properties\PropertyReflectionFinder; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class NullCoalesceRuleTest extends \PHPStan\Testing\RuleTestCase +class NullCoalesceRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + private bool $treatPhpDocTypesAsCertain; + + protected function getRule(): Rule + { + return new NullCoalesceRule(new IssetCheck( + new PropertyDescriptor(), + new PropertyReflectionFinder(), + true, + $this->treatPhpDocTypesAsCertain, + )); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool { - return new NullCoalesceRule(new IssetCheck(new PropertyDescriptor(), new PropertyReflectionFinder())); + return $this->treatPhpDocTypesAsCertain; } public function testCoalesceRule(): void { - $this->analyse([__DIR__ . '/data/null-coalesce.php'], [ + $this->treatPhpDocTypesAsCertain = true; + $errors = [ [ 'Property CoalesceRule\FooCoalesce::$string (string) on left side of ?? is not nullable.', 32, ], [ - 'Offset \'string\' on array(1, 2, 3) on left side of ?? does not exist.', + 'Variable $scalar on left side of ?? always exists and is not nullable.', + 41, + ], + [ + 'Offset \'string\' on array{1, 2, 3} on left side of ?? does not exist.', 45, ], [ - 'Offset \'string\' on array(array(1), array(2), array(3)) on left side of ?? does not exist.', + 'Offset \'string\' on array{array{1}, array{2}, array{3}} on left side of ?? does not exist.', 49, ], [ - 'Offset \'dim\' on array(\'dim\' => 1, \'dim-null\' => 1|null, \'dim-null-offset\' => array(\'a\' => true|null), \'dim-empty\' => array()) on left side of ?? always exists and is not nullable.', + 'Variable $doesNotExist on left side of ?? is never defined.', + 51, + ], + [ + 'Offset \'dim\' on array{dim: 1, dim-null: 1|null, dim-null-offset: array{a: true|null}, dim-empty: array{}} on left side of ?? always exists and is not nullable.', 67, ], [ - 'Offset \'b\' on array() on left side of ?? does not exist.', + 'Offset \'dim-null-not-set\' on array{dim: 1, dim-null: 1|null, dim-null-offset: array{a: true|null}, dim-empty: array{}} on left side of ?? does not exist.', + 73, + ], + [ + 'Offset \'b\' on array{} on left side of ?? does not exist.', 79, ], [ @@ -64,6 +92,10 @@ public function testCoalesceRule(): void 'Static property CoalesceRule\FooCoalesce::$staticAlwaysNull (null) on left side of ?? is always null.', 101, ], + [ + 'Variable $a on left side of ?? always exists and is always null.', + 115, + ], [ 'Property CoalesceRule\FooCoalesce::$string (string) on left side of ?? is not nullable.', 120, @@ -88,11 +120,22 @@ public function testCoalesceRule(): void 'Static property CoalesceRule\FooCoalesce::$staticString (string) on left side of ?? is not nullable.', 131, ], - [ + ]; + if (PHP_VERSION_ID < 80100) { + $errors[] = [ 'Property ReflectionClass::$name (class-string) on left side of ?? is not nullable.', 136, - ], - ]); + ]; + } + $errors[] = [ + 'Variable $foo on left side of ?? is never defined.', + 141, + ]; + $errors[] = [ + 'Variable $bar on left side of ?? is never defined.', + 143, + ]; + $this->analyse([__DIR__ . '/data/null-coalesce.php'], $errors); } public function testCoalesceAssignRule(): void @@ -101,25 +144,38 @@ public function testCoalesceAssignRule(): void $this->markTestSkipped('Test requires PHP 7.4.'); } + $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/null-coalesce-assign.php'], [ [ 'Property CoalesceAssignRule\FooCoalesce::$string (string) on left side of ??= is not nullable.', 32, ], [ - 'Offset \'string\' on array(1, 2, 3) on left side of ??= does not exist.', + 'Variable $scalar on left side of ??= always exists and is not nullable.', + 41, + ], + [ + 'Offset \'string\' on array{1, 2, 3} on left side of ??= does not exist.', 45, ], [ - 'Offset \'string\' on array(array(1), array(2), array(3)) on left side of ??= does not exist.', + 'Offset \'string\' on array{array{1}, array{2}, array{3}} on left side of ??= does not exist.', 49, ], [ - 'Offset \'dim\' on array(\'dim\' => 1, \'dim-null\' => 1|null, \'dim-null-offset\' => array(\'a\' => true|null), \'dim-empty\' => array()) on left side of ??= always exists and is not nullable.', + 'Variable $doesNotExist on left side of ??= is never defined.', + 51, + ], + [ + 'Offset \'dim\' on array{dim: 1, dim-null: 1|null, dim-null-offset: array{a: true|null}, dim-empty: array{}} on left side of ??= always exists and is not nullable.', 67, ], [ - 'Offset \'b\' on array() on left side of ??= does not exist.', + 'Offset \'dim-null-not-set\' on array{dim: 1, dim-null: 0|1, dim-null-offset: array{a: true|null}, dim-empty: array{}} on left side of ??= does not exist.', + 73, + ], + [ + 'Offset \'b\' on array{} on left side of ??= does not exist.', 79, ], [ @@ -142,6 +198,10 @@ public function testCoalesceAssignRule(): void 'Static property CoalesceAssignRule\FooCoalesce::$staticAlwaysNull (null) on left side of ??= is always null.', 101, ], + [ + 'Variable $a on left side of ??= always exists and is always null.', + 115, + ], ]); } @@ -151,7 +211,61 @@ public function testNullsafe(): void $this->markTestSkipped('Test requires PHP 8.0.'); } + $this->treatPhpDocTypesAsCertain = true; $this->analyse([__DIR__ . '/data/null-coalesce-nullsafe.php'], []); } + public function testVariableCertaintyInNullCoalesce(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/variable-certainty-null.php'], [ + [ + 'Variable $scalar on left side of ?? always exists and is not nullable.', + 6, + ], + [ + 'Variable $doesNotExist on left side of ?? is never defined.', + 8, + ], + [ + 'Variable $a on left side of ?? always exists and is always null.', + 13, + ], + ]); + } + + public function testVariableCertaintyInNullCoalesceAssign(): void + { + if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { + $this->markTestSkipped('Test requires PHP 7.4.'); + } + + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/variable-certainty-null-assign.php'], [ + [ + 'Variable $scalar on left side of ??= always exists and is not nullable.', + 6, + ], + [ + 'Variable $doesNotExist on left side of ??= is never defined.', + 8, + ], + [ + 'Variable $a on left side of ??= always exists and is always null.', + 13, + ], + ]); + } + + public function testNullCoalesceInGlobalScope(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/null-coalesce-global-scope.php'], [ + [ + 'Variable $bar on left side of ?? always exists and is not nullable.', + 6, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Variables/ThrowTypeRuleTest.php b/tests/PHPStan/Rules/Variables/ThrowTypeRuleTest.php index f674af7523..0e18f94b78 100644 --- a/tests/PHPStan/Rules/Variables/ThrowTypeRuleTest.php +++ b/tests/PHPStan/Rules/Variables/ThrowTypeRuleTest.php @@ -2,15 +2,18 @@ namespace PHPStan\Rules\Variables; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class ThrowTypeRuleTest extends \PHPStan\Testing\RuleTestCase +class ThrowTypeRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new ThrowTypeRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false)); } @@ -41,7 +44,7 @@ public function testRule(): void 44, 'Learn more at https://phpstan.org/user-guide/discovering-symbols', ], - ] + ], ); } @@ -50,4 +53,18 @@ 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/Variables/UnsetRuleTest.php b/tests/PHPStan/Rules/Variables/UnsetRuleTest.php index f1903324ce..370fe14ea6 100644 --- a/tests/PHPStan/Rules/Variables/UnsetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/UnsetRuleTest.php @@ -2,13 +2,16 @@ namespace PHPStan\Rules\Variables; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; + /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class UnsetRuleTest extends \PHPStan\Testing\RuleTestCase +class UnsetRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new UnsetRule(); } @@ -33,10 +36,6 @@ public function testUnsetRule(): void 'Cannot unset offset \'c\' on 1.', 18, ], - [ - 'Cannot unset offset \'b\' on 1.', - 18, - ], [ 'Cannot unset offset \'string\' on iterable.', 31, diff --git a/tests/PHPStan/Rules/Variables/VariableCertaintyInIssetRuleTest.php b/tests/PHPStan/Rules/Variables/VariableCertaintyInIssetRuleTest.php deleted file mode 100644 index 6d588de94a..0000000000 --- a/tests/PHPStan/Rules/Variables/VariableCertaintyInIssetRuleTest.php +++ /dev/null @@ -1,109 +0,0 @@ - - */ -class VariableCertaintyInIssetRuleTest extends \PHPStan\Testing\RuleTestCase -{ - - protected function getRule(): \PHPStan\Rules\Rule - { - return new VariableCertaintyInIssetRule(); - } - - public function testVariableCertaintyInIsset(): void - { - $this->analyse([__DIR__ . '/data/variable-certainty-isset.php'], [ - [ - 'Variable $alwaysDefinedNotNullable in isset() always exists and is not nullable.', - 14, - ], - [ - 'Variable $neverDefinedVariable in isset() is never defined.', - 22, - ], - [ - 'Variable $anotherNeverDefinedVariable in isset() is never defined.', - 42, - ], - [ - 'Variable $yetAnotherNeverDefinedVariable in isset() is never defined.', - 46, - ], - [ - 'Variable $yetYetAnotherNeverDefinedVariableInIsset in isset() is never defined.', - 56, - ], - [ - 'Variable $anotherVariableInDoWhile in isset() always exists and is not nullable.', - 104, - ], - [ - 'Variable $variableInSecondCase in isset() is never defined.', - 110, - ], - [ - 'Variable $variableInFirstCase in isset() always exists and is not nullable.', - 112, - ], - [ - 'Variable $variableInFirstCase in isset() always exists and is not nullable.', - 116, - ], - [ - 'Variable $variableInSecondCase in isset() always exists and is not nullable.', - 117, - ], - [ - 'Variable $variableAssignedInSecondCase in isset() is never defined.', - 119, - ], - [ - 'Variable $alwaysDefinedForSwitchCondition in isset() always exists and is not nullable.', - 139, - ], - [ - 'Variable $alwaysDefinedForCaseNodeCondition in isset() always exists and is not nullable.', - 140, - ], - [ - 'Variable $alwaysDefinedNotNullable in isset() always exists and is not nullable.', - 152, - ], - [ - 'Variable $neverDefinedVariable in isset() is never defined.', - 152, - ], - [ - 'Variable $a in isset() always exists and is not nullable.', - 214, - ], - [ - 'Variable $null in isset() is always null.', - 225, - ], - ]); - } - - public function testIssetInGlobalScope(): void - { - $this->analyse([__DIR__ . '/data/isset-global-scope.php'], [ - [ - 'Variable $alwaysDefinedNotNullable in isset() always exists and is not nullable.', - 8, - ], - ]); - } - - public function testNullsafe(): void - { - if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) { - $this->markTestSkipped('Test requires PHP 8.0.'); - } - - $this->analyse([__DIR__ . '/data/isset-nullsafe.php'], []); - } - -} diff --git a/tests/PHPStan/Rules/Variables/VariableCertaintyNullCoalesceRuleTest.php b/tests/PHPStan/Rules/Variables/VariableCertaintyNullCoalesceRuleTest.php deleted file mode 100644 index c9c7fe7b22..0000000000 --- a/tests/PHPStan/Rules/Variables/VariableCertaintyNullCoalesceRuleTest.php +++ /dev/null @@ -1,66 +0,0 @@ - - */ -class VariableCertaintyNullCoalesceRuleTest extends \PHPStan\Testing\RuleTestCase -{ - - protected function getRule(): \PHPStan\Rules\Rule - { - return new VariableCertaintyNullCoalesceRule(); - } - - public function testVariableCertaintyInNullCoalesce(): void - { - $this->analyse([__DIR__ . '/data/variable-certainty-null.php'], [ - [ - 'Variable $scalar on left side of ?? always exists and is not nullable.', - 6, - ], - [ - 'Variable $doesNotExist on left side of ?? is never defined.', - 8, - ], - [ - 'Variable $a on left side of ?? is always null.', - 13, - ], - ]); - } - - public function testVariableCertaintyInNullCoalesceAssign(): void - { - if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) { - $this->markTestSkipped('Test requires PHP 7.4.'); - } - - $this->analyse([__DIR__ . '/data/variable-certainty-null-assign.php'], [ - [ - 'Variable $scalar on left side of ??= always exists and is not nullable.', - 6, - ], - [ - 'Variable $doesNotExist on left side of ??= is never defined.', - 8, - ], - [ - 'Variable $a on left side of ??= is always null.', - 13, - ], - ]); - } - - public function testNullCoalesceInGlobalScope(): void - { - $this->analyse([__DIR__ . '/data/null-coalesce-global-scope.php'], [ - [ - 'Variable $bar on left side of ?? always exists and is not nullable.', - 6, - ], - ]); - } - -} diff --git a/tests/PHPStan/Rules/Variables/VariableCloningRuleTest.php b/tests/PHPStan/Rules/Variables/VariableCloningRuleTest.php index 4371c48567..e7c170f694 100644 --- a/tests/PHPStan/Rules/Variables/VariableCloningRuleTest.php +++ b/tests/PHPStan/Rules/Variables/VariableCloningRuleTest.php @@ -2,15 +2,18 @@ namespace PHPStan\Rules\Variables; +use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** - * @extends \PHPStan\Testing\RuleTestCase + * @extends RuleTestCase */ -class VariableCloningRuleTest extends \PHPStan\Testing\RuleTestCase +class VariableCloningRuleTest extends RuleTestCase { - protected function getRule(): \PHPStan\Rules\Rule + protected function getRule(): Rule { return new VariableCloningRule(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false)); } @@ -42,4 +45,18 @@ public function testClone(): void ]); } + public function testRuleWithNullsafeVariant(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/variable-cloning-nullsafe.php'], [ + [ + 'Cannot clone stdClass|null.', + 11, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-3283.php b/tests/PHPStan/Rules/Variables/data/bug-3283.php new file mode 100644 index 0000000000..6fc95b5d2d --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-3283.php @@ -0,0 +1,15 @@ + 5) { + $user = new \stdClass; + $user->name = 'Thibaud'; + } + + echo $user->name ?? 'Default'; +}; diff --git a/tests/PHPStan/Rules/Variables/data/bug-6112.php b/tests/PHPStan/Rules/Variables/data/bug-6112.php new file mode 100644 index 0000000000..2d0d580e58 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-6112.php @@ -0,0 +1,20 @@ += 8.1 + +namespace FirstClassCallablesDefinedVariables; + +class Foo +{ + + public function doFoo(): void + { + $foo->doFoo(); + $foo->doFoo(...); + } + + public function doBar(object $o): void + { + $o->doFoo(...); + ($p = $o)->doFoo(...); + $p->doFoo(); + $p->doFoo(...); + } + +} + +class Bar +{ + + public function doFoo(): void + { + $foo::doFoo(); + $foo::doFoo(...); + } + + public function doBar(object $o): void + { + $o::doFoo(...); + ($p = $o)::doFoo(...); + $p::doFoo(); + $p::doFoo(...); + } + +} + +class Baz +{ + + public function doFoo(): void + { + $foo(); + $foo(...); + } + + public function doBar(object $o): void + { + $o(...); + ($p = $o)(...); + $p(); + $p(...); + } + +} diff --git a/tests/PHPStan/Rules/Variables/data/foreach.php b/tests/PHPStan/Rules/Variables/data/foreach.php index 7d145465d8..7b050056f5 100644 --- a/tests/PHPStan/Rules/Variables/data/foreach.php +++ b/tests/PHPStan/Rules/Variables/data/foreach.php @@ -107,6 +107,35 @@ function (array $arr) { }; +function (array $arr) { + + if (sizeof($arr) === 0) { + return; + } + + foreach ($arr as $val) { + $test = 1; + } + + echo $val; + echo $test; + +}; + +function (array $arr) { + + if (sizeof($arr) === 0) { + return; + } + + if ($arr) { + $test = 1; + } + + echo $test; + +}; + /*function (array $arr) { if (count($arr) > 0) { diff --git a/tests/PHPStan/Rules/Variables/data/isset.php b/tests/PHPStan/Rules/Variables/data/isset.php index 508bbac2ce..b937d47f97 100644 --- a/tests/PHPStan/Rules/Variables/data/isset.php +++ b/tests/PHPStan/Rules/Variables/data/isset.php @@ -160,3 +160,15 @@ function numericStringOffset(string $code): string throw new \RuntimeException(); } + +/** + * @param array{foo: string} $array + * @param 'bar' $bar + */ +function offsetFromPhpdoc(array $array, string $bar) +{ + echo isset($array['foo']) ? $array['foo'] : 0; + + $array = ['bar' => 1]; + echo isset($array[$bar]) ? $array[$bar] : 0; +} diff --git a/tests/PHPStan/Rules/Variables/data/null-coalesce.php b/tests/PHPStan/Rules/Variables/data/null-coalesce.php index a694fa67aa..24d71e8ea0 100644 --- a/tests/PHPStan/Rules/Variables/data/null-coalesce.php +++ b/tests/PHPStan/Rules/Variables/data/null-coalesce.php @@ -136,3 +136,9 @@ function (\ReflectionClass $ref): void { echo $ref->name ?? 'foo'; echo $ref->nonexistent ?? 'bar'; }; + +function (): void { + echo $foo ?? 'foo'; + + echo $bar->bar ?? 'foo'; +}; diff --git a/tests/PHPStan/Rules/Variables/data/throw-values-nullsafe.php b/tests/PHPStan/Rules/Variables/data/throw-values-nullsafe.php new file mode 100644 index 0000000000..24ace167df --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/throw-values-nullsafe.php @@ -0,0 +1,18 @@ += 8.0 + +namespace ThrowValuesNullsafe; + +class Bar +{ + + function doException(): \Exception + { + return new \Exception(); + } + +} + +function doFoo(?Bar $bar) +{ + throw $bar?->doException(); +} diff --git a/tests/PHPStan/Rules/Variables/data/variable-certainty-isset.php b/tests/PHPStan/Rules/Variables/data/variable-certainty-isset.php index 866b7d33e4..d96689ed04 100644 --- a/tests/PHPStan/Rules/Variables/data/variable-certainty-isset.php +++ b/tests/PHPStan/Rules/Variables/data/variable-certainty-isset.php @@ -1,5 +1,5 @@ = 8.0 + +namespace VariableCloningNullsafe; + +class Bar +{ + public \stdClass $foo; +} + +function doFoo(?Bar $bar) { + clone $bar?->foo; +}; diff --git a/tests/PHPStan/Rules/data/datetime-instantiation.php b/tests/PHPStan/Rules/data/datetime-instantiation.php index c529c265de..dada942a30 100644 --- a/tests/PHPStan/Rules/data/datetime-instantiation.php +++ b/tests/PHPStan/Rules/data/datetime-instantiation.php @@ -1,20 +1,20 @@ nullContext = $nullContext; } public function getClass(): string @@ -29,7 +25,7 @@ public function getClass(): string public function isMethodSupported( MethodReflection $methodReflection, MethodCall $node, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): bool { if ($this->nullContext === null) { @@ -47,10 +43,10 @@ public function specifyTypes( MethodReflection $methodReflection, MethodCall $node, Scope $scope, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): SpecifiedTypes { - return new SpecifiedTypes(['$foo' => [$node->args[0]->value, new StringType()]]); + return new SpecifiedTypes(['$foo' => [$node->getArgs()[0]->value, new StringType()]]); } } diff --git a/tests/PHPStan/Tests/AssertionClassStaticMethodTypeSpecifyingExtension.php b/tests/PHPStan/Tests/AssertionClassStaticMethodTypeSpecifyingExtension.php index 0961cd6f8f..8a1a20f8c5 100644 --- a/tests/PHPStan/Tests/AssertionClassStaticMethodTypeSpecifyingExtension.php +++ b/tests/PHPStan/Tests/AssertionClassStaticMethodTypeSpecifyingExtension.php @@ -13,12 +13,8 @@ class AssertionClassStaticMethodTypeSpecifyingExtension implements StaticMethodTypeSpecifyingExtension { - /** @var bool|null */ - private $nullContext; - - public function __construct(?bool $nullContext) + public function __construct(private ?bool $nullContext = null) { - $this->nullContext = $nullContext; } public function getClass(): string @@ -29,7 +25,7 @@ public function getClass(): string public function isStaticMethodSupported( MethodReflection $staticMethodReflection, StaticCall $node, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): bool { if ($this->nullContext === null) { @@ -47,10 +43,10 @@ public function specifyTypes( MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, - TypeSpecifierContext $context + TypeSpecifierContext $context, ): SpecifiedTypes { - return new SpecifiedTypes(['$bar' => [$node->args[0]->value, new IntegerType()]]); + return new SpecifiedTypes(['$bar' => [$node->getArgs()[0]->value, new IntegerType()]]); } } diff --git a/tests/PHPStan/Tests/AssertionException.php b/tests/PHPStan/Tests/AssertionException.php index af86b5a256..0b8f4cbc7c 100644 --- a/tests/PHPStan/Tests/AssertionException.php +++ b/tests/PHPStan/Tests/AssertionException.php @@ -2,7 +2,9 @@ namespace PHPStan\Tests; -class AssertionException extends \Exception +use Exception; + +class AssertionException extends Exception { } diff --git a/tests/PHPStan/TrinaryLogicTest.php b/tests/PHPStan/TrinaryLogicTest.php index def1e3522e..d979c37a84 100644 --- a/tests/PHPStan/TrinaryLogicTest.php +++ b/tests/PHPStan/TrinaryLogicTest.php @@ -2,7 +2,9 @@ namespace PHPStan; -class TrinaryLogicTest extends \PHPStan\Testing\TestCase +use PHPStan\Testing\PHPStanTestCase; + +class TrinaryLogicTest extends PHPStanTestCase { public function dataAnd(): array @@ -28,14 +30,11 @@ public function dataAnd(): array /** * @dataProvider dataAnd - * @param TrinaryLogic $expectedResult - * @param TrinaryLogic $value - * @param TrinaryLogic ...$operands */ public function testAnd( TrinaryLogic $expectedResult, TrinaryLogic $value, - TrinaryLogic ...$operands + TrinaryLogic ...$operands, ): void { $this->assertTrue($expectedResult->equals($value->and(...$operands))); @@ -64,14 +63,11 @@ public function dataOr(): array /** * @dataProvider dataOr - * @param TrinaryLogic $expectedResult - * @param TrinaryLogic $value - * @param TrinaryLogic ...$operands */ public function testOr( TrinaryLogic $expectedResult, TrinaryLogic $value, - TrinaryLogic ...$operands + TrinaryLogic ...$operands, ): void { $this->assertTrue($expectedResult->equals($value->or(...$operands))); @@ -88,8 +84,6 @@ public function dataNegate(): array /** * @dataProvider dataNegate - * @param TrinaryLogic $expectedResult - * @param TrinaryLogic $operand */ public function testNegate(TrinaryLogic $expectedResult, TrinaryLogic $operand): void { @@ -137,29 +131,23 @@ public function dataCompareTo(): array /** * @dataProvider dataCompareTo - * @param TrinaryLogic $first - * @param TrinaryLogic $second - * @param TrinaryLogic|null $expected */ public function testCompareTo(TrinaryLogic $first, TrinaryLogic $second, ?TrinaryLogic $expected): void { $this->assertSame( $expected, - $first->compareTo($second) + $first->compareTo($second), ); } /** * @dataProvider dataCompareTo - * @param TrinaryLogic $first - * @param TrinaryLogic $second - * @param TrinaryLogic|null $expected */ public function testCompareToInversed(TrinaryLogic $first, TrinaryLogic $second, ?TrinaryLogic $expected): void { $this->assertSame( $expected, - $second->compareTo($first) + $second->compareTo($first), ); } diff --git a/tests/PHPStan/Type/Accessory/HasMethodTypeTest.php b/tests/PHPStan/Type/Accessory/HasMethodTypeTest.php index 525b1f6f16..a70ff82ceb 100644 --- a/tests/PHPStan/Type/Accessory/HasMethodTypeTest.php +++ b/tests/PHPStan/Type/Accessory/HasMethodTypeTest.php @@ -2,6 +2,10 @@ namespace PHPStan\Type\Accessory; +use Closure; +use DateTime; +use DateTimeImmutable; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\CallableType; use PHPStan\Type\IntersectionType; @@ -13,8 +17,9 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function sprintf; -class HasMethodTypeTest extends \PHPStan\Testing\TestCase +class HasMethodTypeTest extends PHPStanTestCase { public function dataIsSuperTypeOf(): array @@ -37,7 +42,7 @@ public function dataIsSuperTypeOf(): array ], [ new HasMethodType('format'), - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), TrinaryLogic::createYes(), ], [ @@ -47,7 +52,7 @@ public function dataIsSuperTypeOf(): array ], [ new HasMethodType('foo'), - new ObjectType(\Closure::class), + new ObjectType(Closure::class), TrinaryLogic::createNo(), ], [ @@ -88,15 +93,15 @@ public function dataIsSuperTypeOf(): array [ new HasMethodType('format'), new UnionType([ - new ObjectType(\DateTimeImmutable::class), - new ObjectType(\DateTime::class), + new ObjectType(DateTimeImmutable::class), + new ObjectType(DateTime::class), ]), TrinaryLogic::createYes(), ], [ new HasMethodType('format'), new UnionType([ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new ObjectType('UnknownClass'), ]), TrinaryLogic::createMaybe(), @@ -104,15 +109,15 @@ public function dataIsSuperTypeOf(): array [ new HasMethodType('format'), new UnionType([ - new ObjectType(\DateTimeImmutable::class), - new ObjectType(\Closure::class), + new ObjectType(DateTimeImmutable::class), + new ObjectType(Closure::class), ]), TrinaryLogic::createMaybe(), ], [ new HasMethodType('format'), new IntersectionType([ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new IterableType(new MixedType(), new MixedType()), ]), TrinaryLogic::createYes(), @@ -138,9 +143,6 @@ public function dataIsSuperTypeOf(): array /** * @dataProvider dataIsSuperTypeOf - * @param HasMethodType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(HasMethodType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -148,7 +150,7 @@ public function testIsSuperTypeOf(HasMethodType $type, Type $otherType, TrinaryL $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -183,7 +185,7 @@ public function dataIsSubTypeOf(): array ], [ new HasMethodType('format'), - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), TrinaryLogic::createMaybe(), ], ]; @@ -191,9 +193,6 @@ public function dataIsSubTypeOf(): array /** * @dataProvider dataIsSubTypeOf - * @param HasMethodType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSubTypeOf(HasMethodType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -201,15 +200,12 @@ public function testIsSubTypeOf(HasMethodType $type, Type $otherType, TrinaryLog $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSubTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSubTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } /** * @dataProvider dataIsSubTypeOf - * @param HasMethodType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSubTypeOfInversed(HasMethodType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -217,7 +213,7 @@ public function testIsSubTypeOfInversed(HasMethodType $type, Type $otherType, Tr $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/Accessory/HasPropertyTypeTest.php b/tests/PHPStan/Type/Accessory/HasPropertyTypeTest.php index 9e0012051a..79743043b0 100644 --- a/tests/PHPStan/Type/Accessory/HasPropertyTypeTest.php +++ b/tests/PHPStan/Type/Accessory/HasPropertyTypeTest.php @@ -2,6 +2,9 @@ namespace PHPStan\Type\Accessory; +use Closure; +use DateInterval; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\CallableType; use PHPStan\Type\IntersectionType; @@ -13,8 +16,9 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use function sprintf; -class HasPropertyTypeTest extends \PHPStan\Testing\TestCase +class HasPropertyTypeTest extends PHPStanTestCase { public function dataIsSuperTypeOf(): array @@ -32,7 +36,7 @@ public function dataIsSuperTypeOf(): array ], [ new HasPropertyType('d'), - new ObjectType(\DateInterval::class), + new ObjectType(DateInterval::class), TrinaryLogic::createYes(), ], [ @@ -42,7 +46,7 @@ public function dataIsSuperTypeOf(): array ], [ new HasPropertyType('foo'), - new ObjectType(\Closure::class), + new ObjectType(Closure::class), TrinaryLogic::createNo(), ], [ @@ -78,7 +82,7 @@ public function dataIsSuperTypeOf(): array [ new HasPropertyType('d'), new UnionType([ - new ObjectType(\DateInterval::class), + new ObjectType(DateInterval::class), new ObjectType('UnknownClass'), ]), TrinaryLogic::createMaybe(), @@ -104,9 +108,6 @@ public function dataIsSuperTypeOf(): array /** * @dataProvider dataIsSuperTypeOf - * @param HasPropertyType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(HasPropertyType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -114,7 +115,7 @@ public function testIsSuperTypeOf(HasPropertyType $type, Type $otherType, Trinar $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -144,7 +145,7 @@ public function dataIsSubTypeOf(): array ], [ new HasPropertyType('d'), - new ObjectType(\DateInterval::class), + new ObjectType(DateInterval::class), TrinaryLogic::createMaybe(), ], ]; @@ -152,9 +153,6 @@ public function dataIsSubTypeOf(): array /** * @dataProvider dataIsSubTypeOf - * @param HasPropertyType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSubTypeOf(HasPropertyType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -162,15 +160,12 @@ public function testIsSubTypeOf(HasPropertyType $type, Type $otherType, TrinaryL $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSubTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSubTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } /** * @dataProvider dataIsSubTypeOf - * @param HasPropertyType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSubTypeOfInversed(HasPropertyType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -178,7 +173,7 @@ public function testIsSubTypeOfInversed(HasPropertyType $type, Type $otherType, $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/ArrayTypeTest.php b/tests/PHPStan/Type/ArrayTypeTest.php index 1ec0a20c9c..0390892aef 100644 --- a/tests/PHPStan/Type/ArrayTypeTest.php +++ b/tests/PHPStan/Type/ArrayTypeTest.php @@ -2,7 +2,8 @@ namespace PHPStan\Type; -use PHPStan\Broker\Broker; +use PHPStan\Reflection\ReflectionProviderStaticAccessor; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -10,8 +11,10 @@ use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; +use function array_map; +use function sprintf; -class ArrayTypeTest extends \PHPStan\Testing\TestCase +class ArrayTypeTest extends PHPStanTestCase { public function dataIsSuperTypeOf(): array @@ -67,9 +70,6 @@ public function dataIsSuperTypeOf(): array /** * @dataProvider dataIsSuperTypeOf - * @param ArrayType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(ArrayType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -77,13 +77,13 @@ public function testIsSuperTypeOf(ArrayType $type, Type $otherType, TrinaryLogic $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } public function dataAccepts(): array { - $reflectionProvider = Broker::getInstance(); + $reflectionProvider = ReflectionProviderStaticAccessor::getInstance(); return [ [ @@ -92,7 +92,7 @@ public function dataAccepts(): array new ConstantArrayType([], []), new ConstantArrayType( [new ConstantIntegerType(0)], - [new MixedType()] + [new MixedType()], ), new ConstantArrayType([ new ConstantIntegerType(0), @@ -100,7 +100,7 @@ public function dataAccepts(): array ], [ new StringType(), new MixedType(), - ]) + ]), ), TrinaryLogic::createYes(), ], @@ -132,21 +132,18 @@ public function dataAccepts(): array /** * @dataProvider dataAccepts - * @param ArrayType $acceptingType - * @param Type $acceptedType - * @param TrinaryLogic $expectedResult */ public function testAccepts( ArrayType $acceptingType, Type $acceptedType, - TrinaryLogic $expectedResult + TrinaryLogic $expectedResult, ): void { $actualResult = $acceptingType->accepts($acceptedType, true); $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> accepts(%s)', $acceptingType->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $acceptingType->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())), ); } @@ -165,12 +162,10 @@ public function dataDescribe(): array /** * @dataProvider dataDescribe - * @param ArrayType $type - * @param string $expectedDescription */ public function testDescribe( ArrayType $type, - string $expectedDescription + string $expectedDescription, ): void { $this->assertSame($expectedDescription, $type->describe(VerbosityLevel::precise())); @@ -178,24 +173,22 @@ public function testDescribe( public function dataInferTemplateTypes(): array { - $templateType = static function (string $name): Type { - return TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - $name, - new MixedType(), - TemplateTypeVariance::createInvariant() - ); - }; + $templateType = static fn (string $name): Type => TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + $name, + new MixedType(), + TemplateTypeVariance::createInvariant(), + ); return [ 'valid templated item' => [ new ArrayType( new MixedType(), - new ObjectType('DateTime') + new ObjectType('DateTime'), ), new ArrayType( new MixedType(), - $templateType('T') + $templateType('T'), ), ['T' => 'DateTime'], ], @@ -203,7 +196,7 @@ public function dataInferTemplateTypes(): array new MixedType(), new ArrayType( new MixedType(), - $templateType('T') + $templateType('T'), ), [], ], @@ -211,7 +204,7 @@ public function dataInferTemplateTypes(): array new StringType(), new ArrayType( new MixedType(), - $templateType('T') + $templateType('T'), ), [], ], @@ -221,11 +214,11 @@ public function dataInferTemplateTypes(): array new UnionType([ new StringType(), new IntegerType(), - ]) + ]), ), new ArrayType( new MixedType(), - $templateType('T') + $templateType('T'), ), ['T' => 'int|string'], ], @@ -234,16 +227,16 @@ public function dataInferTemplateTypes(): array new StringType(), new ArrayType( new MixedType(), - new StringType() + new StringType(), ), new ArrayType( new MixedType(), - new IntegerType() + new IntegerType(), ), ]), new ArrayType( new MixedType(), - $templateType('T') + $templateType('T'), ), ['T' => 'int|string'], ], @@ -260,9 +253,7 @@ public function testResolveTemplateTypes(Type $received, Type $template, array $ $this->assertSame( $expectedTypes, - array_map(static function (Type $type): string { - return $type->describe(VerbosityLevel::precise()); - }, $result->getTypes()) + array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::precise()), $result->getTypes()), ); } diff --git a/tests/PHPStan/Type/BooleanTypeTest.php b/tests/PHPStan/Type/BooleanTypeTest.php index 0d52f3f64c..375210eea2 100644 --- a/tests/PHPStan/Type/BooleanTypeTest.php +++ b/tests/PHPStan/Type/BooleanTypeTest.php @@ -2,11 +2,13 @@ namespace PHPStan\Type; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use function sprintf; -class BooleanTypeTest extends \PHPStan\Testing\TestCase +class BooleanTypeTest extends PHPStanTestCase { public function dataAccepts(): array @@ -47,9 +49,6 @@ public function dataAccepts(): array /** * @dataProvider dataAccepts - * @param BooleanType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testAccepts(BooleanType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -57,7 +56,7 @@ public function testAccepts(BooleanType $type, Type $otherType, TrinaryLogic $ex $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -96,9 +95,6 @@ public function dataIsSuperTypeOf(): iterable /** * @dataProvider dataIsSuperTypeOf - * @param BooleanType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(BooleanType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -106,7 +102,7 @@ public function testIsSuperTypeOf(BooleanType $type, Type $otherType, TrinaryLog $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -153,9 +149,6 @@ public function dataEquals(): array /** * @dataProvider dataEquals - * @param BooleanType $type - * @param Type $otherType - * @param bool $expectedResult */ public function testEquals(BooleanType $type, Type $otherType, bool $expectedResult): void { @@ -163,7 +156,7 @@ public function testEquals(BooleanType $type, Type $otherType, bool $expectedRes $this->assertSame( $expectedResult, $actualResult, - sprintf('%s->equals(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s->equals(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/CallableTypeTest.php b/tests/PHPStan/Type/CallableTypeTest.php index 166b499817..1f6e0dc379 100644 --- a/tests/PHPStan/Type/CallableTypeTest.php +++ b/tests/PHPStan/Type/CallableTypeTest.php @@ -2,9 +2,12 @@ namespace PHPStan\Type; +use Closure; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\PassedByReference; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\HasMethodType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; @@ -13,8 +16,10 @@ use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; +use function array_map; +use function sprintf; -class CallableTypeTest extends \PHPStan\Testing\TestCase +class CallableTypeTest extends PHPStanTestCase { public function dataIsSuperTypeOf(): array @@ -50,9 +55,6 @@ public function dataIsSuperTypeOf(): array /** * @dataProvider dataIsSuperTypeOf - * @param CallableType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(CallableType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -60,7 +62,7 @@ public function testIsSuperTypeOf(CallableType $type, Type $otherType, TrinaryLo $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -99,17 +101,12 @@ public function dataIsSubTypeOf(): array ], [ new CallableType(), - new IntersectionType([new CallableType()]), - TrinaryLogic::createYes(), - ], - [ - new CallableType(), - new IntersectionType([new StringType()]), + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), TrinaryLogic::createMaybe(), ], [ new CallableType(), - new IntersectionType([new IntegerType()]), + new IntegerType(), TrinaryLogic::createNo(), ], [ @@ -137,9 +134,6 @@ public function dataIsSubTypeOf(): array /** * @dataProvider dataIsSubTypeOf - * @param CallableType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSubTypeOf(CallableType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -147,15 +141,12 @@ public function testIsSubTypeOf(CallableType $type, Type $otherType, TrinaryLogi $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSubTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSubTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } /** * @dataProvider dataIsSubTypeOf - * @param CallableType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSubTypeOfInversed(CallableType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -163,31 +154,27 @@ public function testIsSubTypeOfInversed(CallableType $type, Type $otherType, Tri $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())), ); } public function dataInferTemplateTypes(): array { - $param = static function (Type $type): NativeParameterReflection { - return new NativeParameterReflection( - '', - false, - $type, - PassedByReference::createNo(), - false, - null - ); - }; + $param = static fn (Type $type): NativeParameterReflection => new NativeParameterReflection( + '', + false, + $type, + PassedByReference::createNo(), + false, + null, + ); - $templateType = static function (string $name): Type { - return TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - $name, - new MixedType(), - TemplateTypeVariance::createInvariant() - ); - }; + $templateType = static fn (string $name): Type => TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + $name, + new MixedType(), + TemplateTypeVariance::createInvariant(), + ); return [ 'template param' => [ @@ -195,13 +182,13 @@ public function dataInferTemplateTypes(): array [ $param(new StringType()), ], - new IntegerType() + new IntegerType(), ), new CallableType( [ $param($templateType('T')), ], - new IntegerType() + new IntegerType(), ), ['T' => 'string'], ], @@ -210,13 +197,13 @@ public function dataInferTemplateTypes(): array [ $param(new StringType()), ], - new IntegerType() + new IntegerType(), ), new CallableType( [ $param(new StringType()), ], - $templateType('T') + $templateType('T'), ), ['T' => 'int'], ], @@ -226,14 +213,14 @@ public function dataInferTemplateTypes(): array $param(new StringType()), $param(new ObjectType('DateTime')), ], - new IntegerType() + new IntegerType(), ), new CallableType( [ $param(new StringType()), $param($templateType('A')), ], - $templateType('B') + $templateType('B'), ), ['B' => 'int', 'A' => 'DateTime'], ], @@ -245,7 +232,7 @@ public function dataInferTemplateTypes(): array $param(new StringType()), $param(new ObjectType('DateTime')), ], - new IntegerType() + new IntegerType(), ), ]), new CallableType( @@ -253,7 +240,7 @@ public function dataInferTemplateTypes(): array $param(new StringType()), $param($templateType('A')), ], - $templateType('B') + $templateType('B'), ), ['B' => 'int', 'A' => 'DateTime'], ], @@ -264,7 +251,7 @@ public function dataInferTemplateTypes(): array $param(new StringType()), $param($templateType('A')), ], - $templateType('B') + $templateType('B'), ), [], ], @@ -281,9 +268,7 @@ public function testResolveTemplateTypes(Type $received, Type $template, array $ $this->assertSame( $expectedTypes, - array_map(static function (Type $type): string { - return $type->describe(VerbosityLevel::precise()); - }, $result->getTypes()) + array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::precise()), $result->getTypes()), ); } @@ -348,7 +333,7 @@ public function dataAccepts(): array new ConstantIntegerType(0), new ConstantIntegerType(1), ], [ - new GenericClassStringType(new ObjectType(\Closure::class)), + new GenericClassStringType(new ObjectType(Closure::class)), new ConstantStringType('bind'), ]), TrinaryLogic::createYes(), @@ -358,20 +343,17 @@ public function dataAccepts(): array /** * @dataProvider dataAccepts - * @param \PHPStan\Type\CallableType $type - * @param Type $acceptedType - * @param TrinaryLogic $expectedResult */ public function testAccepts( CallableType $type, Type $acceptedType, - TrinaryLogic $expectedResult + TrinaryLogic $expectedResult, ): void { $this->assertSame( $expectedResult->describe(), $type->accepts($acceptedType, true)->describe(), - sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/ClassStringTypeTest.php b/tests/PHPStan/Type/ClassStringTypeTest.php index 5d715a948c..2d827f7271 100644 --- a/tests/PHPStan/Type/ClassStringTypeTest.php +++ b/tests/PHPStan/Type/ClassStringTypeTest.php @@ -2,12 +2,15 @@ namespace PHPStan\Type; -use PHPStan\Testing\TestCase; +use Exception; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\GenericClassStringType; +use stdClass; +use function sprintf; -class ClassStringTypeTest extends TestCase +class ClassStringTypeTest extends PHPStanTestCase { public function dataIsSuperTypeOf(): array @@ -15,7 +18,7 @@ public function dataIsSuperTypeOf(): array return [ [ new ClassStringType(), - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), TrinaryLogic::createYes(), ], [ @@ -25,7 +28,7 @@ public function dataIsSuperTypeOf(): array ], [ new ClassStringType(), - new ConstantStringType(\stdClass::class), + new ConstantStringType(stdClass::class), TrinaryLogic::createYes(), ], [ @@ -45,7 +48,7 @@ public function testIsSuperTypeOf(ClassStringType $type, Type $otherType, Trinar $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -71,7 +74,7 @@ public function dataAccepts(): iterable yield [ new ClassStringType(), - new ConstantStringType(\stdClass::class), + new ConstantStringType(stdClass::class), TrinaryLogic::createYes(), ]; @@ -83,13 +86,13 @@ public function dataAccepts(): iterable yield [ new ClassStringType(), - new UnionType([new ConstantStringType(\stdClass::class), new ConstantStringType(self::class)]), + new UnionType([new ConstantStringType(stdClass::class), new ConstantStringType(self::class)]), TrinaryLogic::createYes(), ]; yield [ new ClassStringType(), - new UnionType([new ConstantStringType(\stdClass::class), new ConstantStringType('Nonexistent')]), + new UnionType([new ConstantStringType(stdClass::class), new ConstantStringType('Nonexistent')]), TrinaryLogic::createMaybe(), ]; @@ -102,9 +105,6 @@ public function dataAccepts(): iterable /** * @dataProvider dataAccepts - * @param \PHPStan\Type\ClassStringType $type - * @param Type $otherType - * @param \PHPStan\TrinaryLogic $expectedResult */ public function testAccepts(ClassStringType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -112,7 +112,7 @@ public function testAccepts(ClassStringType $type, Type $otherType, TrinaryLogic $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -134,9 +134,6 @@ public function dataEquals(): array /** * @dataProvider dataEquals - * @param ClassStringType $type - * @param Type $otherType - * @param bool $expectedResult */ public function testEquals(ClassStringType $type, Type $otherType, bool $expectedResult): void { @@ -144,7 +141,7 @@ public function testEquals(ClassStringType $type, Type $otherType, bool $expecte $this->assertSame( $expectedResult, $actualResult, - sprintf('%s->equals(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s->equals(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/ClosureTypeTest.php b/tests/PHPStan/Type/ClosureTypeTest.php index c788121178..f8d029048e 100644 --- a/tests/PHPStan/Type/ClosureTypeTest.php +++ b/tests/PHPStan/Type/ClosureTypeTest.php @@ -2,9 +2,12 @@ namespace PHPStan\Type; +use Closure; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use function sprintf; -class ClosureTypeTest extends \PHPStan\Testing\TestCase +class ClosureTypeTest extends PHPStanTestCase { public function dataIsSuperTypeOf(): array @@ -12,7 +15,7 @@ public function dataIsSuperTypeOf(): array return [ [ new ClosureType([], new MixedType(), false), - new ObjectType(\Closure::class), + new ObjectType(Closure::class), TrinaryLogic::createMaybe(), ], [ @@ -31,7 +34,7 @@ public function dataIsSuperTypeOf(): array TrinaryLogic::createMaybe(), ], [ - new ObjectType(\Closure::class), + new ObjectType(Closure::class), new ClosureType([], new MixedType(), false), TrinaryLogic::createYes(), ], @@ -71,13 +74,13 @@ public function dataIsSuperTypeOf(): array TrinaryLogic::createNo(), ], [ - new ObjectWithoutClassType(new ObjectType(\Closure::class)), + new ObjectWithoutClassType(new ObjectType(Closure::class)), new ClosureType([], new MixedType(), false), TrinaryLogic::createNo(), ], [ new ClosureType([], new MixedType(), false), - new ObjectWithoutClassType(new ObjectType(\Closure::class)), + new ObjectWithoutClassType(new ObjectType(Closure::class)), TrinaryLogic::createNo(), ], ]; @@ -85,21 +88,18 @@ public function dataIsSuperTypeOf(): array /** * @dataProvider dataIsSuperTypeOf - * @param Type $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf( Type $type, Type $otherType, - TrinaryLogic $expectedResult + TrinaryLogic $expectedResult, ): void { $actualResult = $type->isSuperTypeOf($otherType); $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php index 8ffce8f7ad..0bad091f2d 100644 --- a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php @@ -2,6 +2,8 @@ namespace PHPStan\Type\Constant; +use Closure; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\ArrayType; use PHPStan\Type\CallableType; @@ -17,8 +19,10 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; +use function array_map; +use function sprintf; -class ConstantArrayTypeTest extends \PHPStan\Testing\TestCase +class ConstantArrayTypeTest extends PHPStanTestCase { public function dataAccepts(): iterable @@ -126,7 +130,7 @@ public function dataAccepts(): iterable ], [ new StringType(), new StringType(), - ]) + ]), ), new ConstantArrayType([ new ConstantStringType('name'), @@ -163,7 +167,7 @@ public function dataAccepts(): iterable ], [ new StringType(), new StringType(), - ]) + ]), ), new ConstantArrayType([ new ConstantStringType('surname'), @@ -344,9 +348,6 @@ public function dataAccepts(): iterable /** * @dataProvider dataAccepts - * @param Type $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testAccepts(Type $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -354,7 +355,7 @@ public function testAccepts(Type $type, Type $otherType, TrinaryLogic $expectedR $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -503,9 +504,6 @@ public function dataIsSuperTypeOf(): iterable /** * @dataProvider dataIsSuperTypeOf - * @param ConstantArrayType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(ConstantArrayType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -513,20 +511,18 @@ public function testIsSuperTypeOf(ConstantArrayType $type, Type $otherType, Trin $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } public function dataInferTemplateTypes(): array { - $templateType = static function (string $name): Type { - return TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - $name, - new MixedType(), - TemplateTypeVariance::createInvariant() - ); - }; + $templateType = static fn (string $name): Type => TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + $name, + new MixedType(), + TemplateTypeVariance::createInvariant(), + ); return [ 'receive constant array' => [ @@ -538,7 +534,7 @@ public function dataInferTemplateTypes(): array [ new StringType(), new IntegerType(), - ] + ], ), new ConstantArrayType( [ @@ -548,7 +544,7 @@ public function dataInferTemplateTypes(): array [ $templateType('T'), $templateType('U'), - ] + ], ), ['T' => 'string', 'U' => 'int'], ], @@ -561,7 +557,7 @@ public function dataInferTemplateTypes(): array [ new StringType(), new IntegerType(), - ] + ], ), new ConstantArrayType( [ @@ -571,7 +567,7 @@ public function dataInferTemplateTypes(): array [ $templateType('T'), $templateType('U'), - ] + ], ), ['T' => 'string', 'U' => 'int'], ], @@ -582,7 +578,7 @@ public function dataInferTemplateTypes(): array ], [ new StringType(), - ] + ], ), new ConstantArrayType( [ @@ -592,7 +588,7 @@ public function dataInferTemplateTypes(): array [ $templateType('T'), $templateType('U'), - ] + ], ), [], ], @@ -604,7 +600,7 @@ public function dataInferTemplateTypes(): array ], [ $templateType('T'), - ] + ], ), [], ], @@ -616,7 +612,7 @@ public function dataInferTemplateTypes(): array ], [ $templateType('T'), - ] + ], ), ['T' => 'string'], ], @@ -633,9 +629,7 @@ public function testResolveTemplateTypes(Type $received, Type $template, array $ $this->assertSame( $expectedTypes, - array_map(static function (Type $type): string { - return $type->describe(VerbosityLevel::precise()); - }, $result->getTypes()) + array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::precise()), $result->getTypes()), ); } @@ -648,7 +642,7 @@ public function testIsCallable(ConstantArrayType $type, TrinaryLogic $expectedRe $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())), ); } @@ -673,7 +667,7 @@ public function dataIsCallable(): iterable new ConstantIntegerType(0), new ConstantIntegerType(1), ], [ - new ConstantStringType(\Closure::class, true), + new ConstantStringType(Closure::class, true), new ConstantStringType('bind'), ]), TrinaryLogic::createYes(), @@ -684,7 +678,7 @@ public function dataIsCallable(): iterable new ConstantIntegerType(0), new ConstantIntegerType(1), ], [ - new ConstantStringType(\Closure::class, true), + new ConstantStringType(Closure::class, true), new ConstantStringType('foobar'), ]), TrinaryLogic::createNo(), @@ -706,7 +700,7 @@ public function dataIsCallable(): iterable new ConstantStringType('a'), new ConstantStringType('b'), ], [ - new ConstantStringType(\Closure::class, true), + new ConstantStringType(Closure::class, true), new ConstantStringType('bind'), ]), TrinaryLogic::createNo(), @@ -717,7 +711,7 @@ public function dataIsCallable(): iterable new ConstantIntegerType(0), new ConstantIntegerType(1), ], [ - new GenericClassStringType(new ObjectType(\Closure::class)), + new GenericClassStringType(new ObjectType(Closure::class)), new ConstantStringType('bind'), ]), TrinaryLogic::createYes(), diff --git a/tests/PHPStan/Type/Constant/ConstantFloatTypeTest.php b/tests/PHPStan/Type/Constant/ConstantFloatTypeTest.php index b8261bbea7..00ec37a8b6 100644 --- a/tests/PHPStan/Type/Constant/ConstantFloatTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantFloatTypeTest.php @@ -2,9 +2,10 @@ namespace PHPStan\Type\Constant; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\VerbosityLevel; -class ConstantFloatTypeTest extends \PHPStan\Testing\TestCase +class ConstantFloatTypeTest extends PHPStanTestCase { public function dataDescribe(): array @@ -31,12 +32,10 @@ public function dataDescribe(): array /** * @dataProvider dataDescribe - * @param ConstantFloatType $type - * @param string $expectedDescription */ public function testDescribe( ConstantFloatType $type, - string $expectedDescription + string $expectedDescription, ): void { $this->assertSame($expectedDescription, $type->describe(VerbosityLevel::precise())); diff --git a/tests/PHPStan/Type/Constant/ConstantIntegerTypeTest.php b/tests/PHPStan/Type/Constant/ConstantIntegerTypeTest.php index 5a23289569..1b39e5ff55 100644 --- a/tests/PHPStan/Type/Constant/ConstantIntegerTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantIntegerTypeTest.php @@ -2,12 +2,14 @@ namespace PHPStan\Type\Constant; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\IntegerType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function sprintf; -class ConstantIntegerTypeTest extends \PHPStan\Testing\TestCase +class ConstantIntegerTypeTest extends PHPStanTestCase { public function dataAccepts(): iterable @@ -33,9 +35,6 @@ public function dataAccepts(): iterable /** * @dataProvider dataAccepts - * @param ConstantIntegerType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testAccepts(ConstantIntegerType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -43,7 +42,7 @@ public function testAccepts(ConstantIntegerType $type, Type $otherType, TrinaryL $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -70,9 +69,6 @@ public function dataIsSuperTypeOf(): iterable /** * @dataProvider dataIsSuperTypeOf - * @param ConstantIntegerType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(ConstantIntegerType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -80,7 +76,7 @@ public function testIsSuperTypeOf(ConstantIntegerType $type, Type $otherType, Tr $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php index 5e28ae9a68..f2be3f6973 100644 --- a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php @@ -2,126 +2,135 @@ namespace PHPStan\Type\Constant; -use PHPStan\Testing\TestCase; +use Exception; +use InvalidArgumentException; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPStan\Type\ErrorType; +use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; +use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; use PHPStan\Type\StaticType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use stdClass; +use Throwable; +use function sprintf; -class ConstantStringTypeTest extends TestCase +class ConstantStringTypeTest extends PHPStanTestCase { public function dataIsSuperTypeOf(): array { + $reflectionProvider = $this->createReflectionProvider(); return [ 0 => [ - new ConstantStringType(\Exception::class), - new GenericClassStringType(new ObjectType(\Exception::class)), + new ConstantStringType(Exception::class), + new GenericClassStringType(new ObjectType(Exception::class)), TrinaryLogic::createMaybe(), ], 1 => [ - new ConstantStringType(\Exception::class), - new GenericClassStringType(new ObjectType(\Throwable::class)), + new ConstantStringType(Exception::class), + new GenericClassStringType(new ObjectType(Throwable::class)), TrinaryLogic::createMaybe(), ], 2 => [ - new ConstantStringType(\Exception::class), - new GenericClassStringType(new ObjectType(\InvalidArgumentException::class)), + new ConstantStringType(Exception::class), + new GenericClassStringType(new ObjectType(InvalidArgumentException::class)), TrinaryLogic::createNo(), ], 3 => [ - new ConstantStringType(\Exception::class), - new GenericClassStringType(new ObjectType(\stdClass::class)), + new ConstantStringType(Exception::class), + new GenericClassStringType(new ObjectType(stdClass::class)), TrinaryLogic::createNo(), ], 4 => [ - new ConstantStringType(\Exception::class), - new ConstantStringType(\Exception::class), + new ConstantStringType(Exception::class), + new ConstantStringType(Exception::class), TrinaryLogic::createYes(), ], 5 => [ - new ConstantStringType(\Exception::class), - new ConstantStringType(\InvalidArgumentException::class), + new ConstantStringType(Exception::class), + new ConstantStringType(InvalidArgumentException::class), TrinaryLogic::createNo(), ], 6 => [ - new ConstantStringType(\Exception::class), - new GenericClassStringType(new ObjectType(\Exception::class)), + new ConstantStringType(Exception::class), + new GenericClassStringType(new ObjectType(Exception::class)), TrinaryLogic::createMaybe(), ], 7 => [ - new ConstantStringType(\Exception::class), - new GenericClassStringType(new ObjectType(\stdClass::class)), + new ConstantStringType(Exception::class), + new GenericClassStringType(new ObjectType(stdClass::class)), TrinaryLogic::createNo(), ], 8 => [ - new ConstantStringType(\Exception::class), + new ConstantStringType(Exception::class), new GenericClassStringType(TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('foo'), 'T', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), )), TrinaryLogic::createMaybe(), ], 9 => [ - new ConstantStringType(\Exception::class), + new ConstantStringType(Exception::class), new GenericClassStringType(TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('foo'), 'T', - new ObjectType(\Exception::class), - TemplateTypeVariance::createInvariant() + new ObjectType(Exception::class), + TemplateTypeVariance::createInvariant(), )), TrinaryLogic::createMaybe(), ], 10 => [ - new ConstantStringType(\InvalidArgumentException::class), + new ConstantStringType(InvalidArgumentException::class), new GenericClassStringType(TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('foo'), 'T', - new ObjectType(\Exception::class), - TemplateTypeVariance::createInvariant() + new ObjectType(Exception::class), + TemplateTypeVariance::createInvariant(), )), TrinaryLogic::createMaybe(), ], 11 => [ - new ConstantStringType(\Throwable::class), + new ConstantStringType(Throwable::class), new GenericClassStringType(TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('foo'), 'T', - new ObjectType(\Exception::class), - TemplateTypeVariance::createInvariant() + new ObjectType(Exception::class), + TemplateTypeVariance::createInvariant(), )), TrinaryLogic::createNo(), ], 12 => [ - new ConstantStringType(\stdClass::class), + new ConstantStringType(stdClass::class), new GenericClassStringType(TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('foo'), 'T', - new ObjectType(\Exception::class), - TemplateTypeVariance::createInvariant() + new ObjectType(Exception::class), + TemplateTypeVariance::createInvariant(), )), TrinaryLogic::createNo(), ], 13 => [ - new ConstantStringType(\Exception::class), - new GenericClassStringType(new StaticType(\Exception::class)), + new ConstantStringType(Exception::class), + new GenericClassStringType(new StaticType($reflectionProvider->getClass(Exception::class))), TrinaryLogic::createMaybe(), ], 14 => [ - new ConstantStringType(\Exception::class), - new GenericClassStringType(new StaticType(\InvalidArgumentException::class)), + new ConstantStringType(Exception::class), + new GenericClassStringType(new StaticType($reflectionProvider->getClass(InvalidArgumentException::class))), TrinaryLogic::createNo(), ], 15 => [ - new ConstantStringType(\Exception::class), - new GenericClassStringType(new StaticType(\Throwable::class)), + new ConstantStringType(Exception::class), + new GenericClassStringType(new StaticType($reflectionProvider->getClass(Throwable::class))), TrinaryLogic::createMaybe(), ], ]; @@ -136,16 +145,20 @@ public function testIsSuperTypeOf(ConstantStringType $type, Type $otherType, Tri $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } public function testGeneralize(): void { - $this->assertSame('string', (new ConstantStringType('NonexistentClass'))->generalize()->describe(VerbosityLevel::precise())); - $this->assertSame('string', (new ConstantStringType(\stdClass::class))->generalize()->describe(VerbosityLevel::precise())); - $this->assertSame('class-string', (new ConstantStringType(\stdClass::class, true))->generalize()->describe(VerbosityLevel::precise())); - $this->assertSame('class-string', (new ConstantStringType('NonexistentClass', true))->generalize()->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&non-empty-string', (new ConstantStringType('NonexistentClass'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string', (new ConstantStringType(''))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&non-empty-string', (new ConstantStringType('a'))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('string', (new ConstantStringType(''))->generalize(GeneralizePrecision::lessSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('string', (new ConstantStringType('a'))->generalize(GeneralizePrecision::lessSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('literal-string&non-empty-string', (new ConstantStringType(stdClass::class))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('class-string', (new ConstantStringType(stdClass::class, true))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); + $this->assertSame('class-string', (new ConstantStringType('NonexistentClass', true))->generalize(GeneralizePrecision::moreSpecific())->describe(VerbosityLevel::precise())); } public function testTextInvalidEncoding(): void @@ -158,4 +171,11 @@ public function testShortTextInvalidEncoding(): void $this->assertSame("'\xc3Lorem ipsum dolor'", (new ConstantStringType("\xc3Lorem ipsum dolor"))->describe(VerbosityLevel::value())); } + public function testSetInvalidValue(): void + { + $string = new ConstantStringType('internal:/node/add'); + $result = $string->setOffsetValueType(new ConstantIntegerType(0), new NullType()); + $this->assertInstanceOf(ErrorType::class, $result); + } + } diff --git a/tests/PHPStan/Type/Enum/EnumCaseObjectTypeTest.php b/tests/PHPStan/Type/Enum/EnumCaseObjectTypeTest.php new file mode 100644 index 0000000000..e700d67a3d --- /dev/null +++ b/tests/PHPStan/Type/Enum/EnumCaseObjectTypeTest.php @@ -0,0 +1,227 @@ +markTestSkipped('Test requires PHP 8.1.'); + } + $actualResult = $type->isSuperTypeOf($otherType); + $this->assertSame( + $expectedResult->describe(), + $actualResult->describe(), + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), + ); + } + + public function dataAccepts(): iterable + { + yield [ + new ObjectType('PHPStan\Fixture\TestEnum'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + TrinaryLogic::createYes(), + ]; + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + TrinaryLogic::createYes(), + ]; + + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), + TrinaryLogic::createNo(), + ]; + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new ObjectType('PHPStan\Fixture\TestEnum'), + TrinaryLogic::createMaybe(), + ]; + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new ObjectType('PHPStan\Fixture\TestEnumInterface'), + TrinaryLogic::createMaybe(), + ]; + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new ObjectType('stdClass'), + TrinaryLogic::createNo(), + ]; + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new ObjectType(FinalClass::class), + TrinaryLogic::createNo(), + ]; + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new ObjectType('Stringable'), + TrinaryLogic::createNo(), + ]; + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new ObjectWithoutClassType(), + TrinaryLogic::createMaybe(), + ]; + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new ObjectWithoutClassType(new ObjectType('PHPStan\Fixture\TestEnum')), + TrinaryLogic::createNo(), + ]; + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new ObjectWithoutClassType(new ObjectType('PHPStan\Fixture\TestEnumInterface')), + TrinaryLogic::createNo(), + ]; + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new ObjectType('PHPStan\Fixture\TestEnumInterface', new ObjectType('PHPStan\Fixture\TestEnum')), + TrinaryLogic::createNo(), + ]; + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new ObjectWithoutClassType(new ObjectType('PHPStan\Fixture\AnotherTestEnum')), + TrinaryLogic::createMaybe(), + ]; + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), + ]), + TrinaryLogic::createMaybe(), + ]; + yield [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new UnionType([ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'THREE'), + ]), + TrinaryLogic::createNo(), + ]; + } + + /** + * @dataProvider dataAccepts + */ + public function testAccepts( + Type $type, + Type $acceptedType, + TrinaryLogic $expectedResult, + ): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->assertSame( + $expectedResult->describe(), + $type->accepts($acceptedType, true)->describe(), + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())), + ); + } + +} diff --git a/tests/PHPStan/Type/FileTypeMapperTest.php b/tests/PHPStan/Type/FileTypeMapperTest.php index a9b34cf944..cc8a6632b6 100644 --- a/tests/PHPStan/Type/FileTypeMapperTest.php +++ b/tests/PHPStan/Type/FileTypeMapperTest.php @@ -2,7 +2,15 @@ namespace PHPStan\Type; -class FileTypeMapperTest extends \PHPStan\Testing\TestCase +use DependentPhpDocs\Foo; +use PHPStan\Broker\Broker; +use PHPStan\PhpDoc\Tag\ReturnTag; +use PHPStan\ShouldNotHappenException; +use PHPStan\Testing\PHPStanTestCase; +use RuntimeException; +use function realpath; + +class FileTypeMapperTest extends PHPStanTestCase { public function testGetResolvedPhpDoc(): void @@ -85,21 +93,21 @@ public function testFileWithDependentPhpDocs(): void $realpath = realpath(__DIR__ . '/data/dependent-phpdocs.php'); if ($realpath === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $resolved = $fileTypeMapper->getResolvedPhpDoc( $realpath, - \DependentPhpDocs\Foo::class, + Foo::class, null, 'addPages', - '/** @param Foo[]|Foo|\Iterator $pages */' + '/** @param Foo[]|Foo|\Iterator $pages */', ); $this->assertCount(1, $resolved->getParamTags()); $this->assertSame( '(DependentPhpDocs\Foo&iterable)|(iterable&Iterator)', - $resolved->getParamTags()['pages']->getType()->describe(VerbosityLevel::precise()) + $resolved->getParamTags()['pages']->getType()->describe(VerbosityLevel::precise()), ); } @@ -110,7 +118,7 @@ public function testFileThrowsPhpDocs(): void $realpath = realpath(__DIR__ . '/data/throws-phpdocs.php'); if ($realpath === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $resolved = $fileTypeMapper->getResolvedPhpDoc($realpath, \ThrowsPhpDocs\Foo::class, null, 'throwRuntimeException', '/** @@ -119,8 +127,8 @@ public function testFileThrowsPhpDocs(): void $this->assertNotNull($resolved->getThrowsTag()); $this->assertSame( - \RuntimeException::class, - $resolved->getThrowsTag()->getType()->describe(VerbosityLevel::precise()) + RuntimeException::class, + $resolved->getThrowsTag()->getType()->describe(VerbosityLevel::precise()), ); $resolved = $fileTypeMapper->getResolvedPhpDoc($realpath, \ThrowsPhpDocs\Foo::class, null, 'throwRuntimeAndLogicException', '/** @@ -130,7 +138,7 @@ public function testFileThrowsPhpDocs(): void $this->assertNotNull($resolved->getThrowsTag()); $this->assertSame( 'LogicException|RuntimeException', - $resolved->getThrowsTag()->getType()->describe(VerbosityLevel::precise()) + $resolved->getThrowsTag()->getType()->describe(VerbosityLevel::precise()), ); $resolved = $fileTypeMapper->getResolvedPhpDoc($realpath, \ThrowsPhpDocs\Foo::class, null, 'throwRuntimeAndLogicException2', '/** @@ -141,20 +149,20 @@ public function testFileThrowsPhpDocs(): void $this->assertNotNull($resolved->getThrowsTag()); $this->assertSame( 'LogicException|RuntimeException', - $resolved->getThrowsTag()->getType()->describe(VerbosityLevel::precise()) + $resolved->getThrowsTag()->getType()->describe(VerbosityLevel::precise()), ); } public function testFileWithCyclicPhpDocs(): void { - self::getContainer()->getByType(\PHPStan\Broker\Broker::class); + self::getContainer()->getByType(Broker::class); /** @var FileTypeMapper $fileTypeMapper */ $fileTypeMapper = self::getContainer()->getByType(FileTypeMapper::class); $realpath = realpath(__DIR__ . '/data/cyclic-phpdocs.php'); if ($realpath === false) { - throw new \PHPStan\ShouldNotHappenException(); + throw new ShouldNotHappenException(); } $resolved = $fileTypeMapper->getResolvedPhpDoc( @@ -162,10 +170,10 @@ public function testFileWithCyclicPhpDocs(): void \CyclicPhpDocs\Foo::class, null, 'getIterator', - '/** @return iterable | Foo */' + '/** @return iterable | Foo */', ); - /** @var \PHPStan\PhpDoc\Tag\ReturnTag $returnTag */ + /** @var ReturnTag $returnTag */ $returnTag = $resolved->getReturnTag(); $this->assertSame('CyclicPhpDocs\Foo|iterable', $returnTag->getType()->describe(VerbosityLevel::precise())); } diff --git a/tests/PHPStan/Type/FloatTypeTest.php b/tests/PHPStan/Type/FloatTypeTest.php index 903beaf221..ef878bf6dd 100644 --- a/tests/PHPStan/Type/FloatTypeTest.php +++ b/tests/PHPStan/Type/FloatTypeTest.php @@ -2,12 +2,14 @@ namespace PHPStan\Type; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use function sprintf; -class FloatTypeTest extends \PHPStan\Testing\TestCase +class FloatTypeTest extends PHPStanTestCase { public function dataAccepts(): array @@ -60,8 +62,6 @@ public function dataAccepts(): array /** * @dataProvider dataAccepts - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testAccepts(Type $otherType, TrinaryLogic $expectedResult): void { @@ -70,7 +70,7 @@ public function testAccepts(Type $otherType, TrinaryLogic $expectedResult): void $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -122,9 +122,6 @@ public function dataEquals(): array /** * @dataProvider dataEquals - * @param FloatType $type - * @param Type $otherType - * @param bool $expectedResult */ public function testEquals(FloatType $type, Type $otherType, bool $expectedResult): void { @@ -132,7 +129,7 @@ public function testEquals(FloatType $type, Type $otherType, bool $expectedResul $this->assertSame( $expectedResult, $actualResult, - sprintf('%s->equals(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s->equals(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/Generic/GenericClassStringTypeTest.php b/tests/PHPStan/Type/Generic/GenericClassStringTypeTest.php index 277a3875f5..bfd00bcbb1 100644 --- a/tests/PHPStan/Type/Generic/GenericClassStringTypeTest.php +++ b/tests/PHPStan/Type/Generic/GenericClassStringTypeTest.php @@ -2,6 +2,10 @@ namespace PHPStan\Type\Generic; +use DateTime; +use Exception; +use InvalidArgumentException; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\ClassStringType; @@ -14,61 +18,66 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use stdClass; +use Throwable; +use function sprintf; -class GenericClassStringTypeTest extends \PHPStan\Testing\TestCase +class GenericClassStringTypeTest extends PHPStanTestCase { public function dataIsSuperTypeOf(): array { + $reflectionProvider = $this->createReflectionProvider(); + return [ 0 => [ - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), new ClassStringType(), TrinaryLogic::createMaybe(), ], 1 => [ - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), new StringType(), TrinaryLogic::createMaybe(), ], 2 => [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), TrinaryLogic::createYes(), ], 3 => [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\Throwable::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(Throwable::class)), TrinaryLogic::createMaybe(), ], 4 => [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\InvalidArgumentException::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(InvalidArgumentException::class)), TrinaryLogic::createYes(), ], 5 => [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\stdClass::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(stdClass::class)), TrinaryLogic::createNo(), ], 6 => [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new ConstantStringType(\Exception::class), + new GenericClassStringType(new ObjectType(Exception::class)), + new ConstantStringType(Exception::class), TrinaryLogic::createYes(), ], 7 => [ - new GenericClassStringType(new ObjectType(\Throwable::class)), - new ConstantStringType(\Exception::class), + new GenericClassStringType(new ObjectType(Throwable::class)), + new ConstantStringType(Exception::class), TrinaryLogic::createYes(), ], 8 => [ - new GenericClassStringType(new ObjectType(\InvalidArgumentException::class)), - new ConstantStringType(\Exception::class), + new GenericClassStringType(new ObjectType(InvalidArgumentException::class)), + new ConstantStringType(Exception::class), TrinaryLogic::createNo(), ], 9 => [ - new GenericClassStringType(new ObjectType(\stdClass::class)), - new ConstantStringType(\Exception::class), + new GenericClassStringType(new ObjectType(stdClass::class)), + new ConstantStringType(Exception::class), TrinaryLogic::createNo(), ], 10 => [ @@ -76,64 +85,64 @@ public function dataIsSuperTypeOf(): array TemplateTypeScope::createWithFunction('foo'), 'T', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), )), - new ConstantStringType(\Exception::class), + new ConstantStringType(Exception::class), TrinaryLogic::createYes(), ], 11 => [ new GenericClassStringType(TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('foo'), 'T', - new ObjectType(\Exception::class), - TemplateTypeVariance::createInvariant() + new ObjectType(Exception::class), + TemplateTypeVariance::createInvariant(), )), - new ConstantStringType(\Exception::class), + new ConstantStringType(Exception::class), TrinaryLogic::createYes(), ], 12 => [ new GenericClassStringType(TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('foo'), 'T', - new ObjectType(\Exception::class), - TemplateTypeVariance::createInvariant() + new ObjectType(Exception::class), + TemplateTypeVariance::createInvariant(), )), - new ConstantStringType(\stdClass::class), + new ConstantStringType(stdClass::class), TrinaryLogic::createNo(), ], 13 => [ new GenericClassStringType(TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('foo'), 'T', - new ObjectType(\Exception::class), - TemplateTypeVariance::createInvariant() + new ObjectType(Exception::class), + TemplateTypeVariance::createInvariant(), )), - new ConstantStringType(\InvalidArgumentException::class), + new ConstantStringType(InvalidArgumentException::class), TrinaryLogic::createYes(), ], 14 => [ new GenericClassStringType(TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('foo'), 'T', - new ObjectType(\Exception::class), - TemplateTypeVariance::createInvariant() + new ObjectType(Exception::class), + TemplateTypeVariance::createInvariant(), )), - new ConstantStringType(\Throwable::class), + new ConstantStringType(Throwable::class), TrinaryLogic::createNo(), ], 15 => [ - new GenericClassStringType(new StaticType(\Exception::class)), - new ConstantStringType(\Exception::class), + new GenericClassStringType(new StaticType($reflectionProvider->getClass(Exception::class))), + new ConstantStringType(Exception::class), TrinaryLogic::createYes(), ], 16 => [ - new GenericClassStringType(new StaticType(\InvalidArgumentException::class)), - new ConstantStringType(\Exception::class), + new GenericClassStringType(new StaticType($reflectionProvider->getClass(InvalidArgumentException::class))), + new ConstantStringType(Exception::class), TrinaryLogic::createNo(), ], 17 => [ - new GenericClassStringType(new StaticType(\Throwable::class)), - new ConstantStringType(\Exception::class), + new GenericClassStringType(new StaticType($reflectionProvider->getClass(Throwable::class))), + new ConstantStringType(Exception::class), TrinaryLogic::createYes(), ], ]; @@ -148,7 +157,7 @@ public function testIsSuperTypeOf(GenericClassStringType $type, Type $otherType, $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -156,33 +165,33 @@ public function dataAccepts(): array { return [ 0 => [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new ConstantStringType(\Throwable::class), + new GenericClassStringType(new ObjectType(Exception::class)), + new ConstantStringType(Throwable::class), TrinaryLogic::createNo(), ], 1 => [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new ConstantStringType(\Exception::class), + new GenericClassStringType(new ObjectType(Exception::class)), + new ConstantStringType(Exception::class), TrinaryLogic::createYes(), ], 2 => [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new ConstantStringType(\InvalidArgumentException::class), + new GenericClassStringType(new ObjectType(Exception::class)), + new ConstantStringType(InvalidArgumentException::class), TrinaryLogic::createYes(), ], 3 => [ - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), new StringType(), TrinaryLogic::createMaybe(), ], 4 => [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new ObjectType(\Exception::class), + new GenericClassStringType(new ObjectType(Exception::class)), + new ObjectType(Exception::class), TrinaryLogic::createNo(), ], 5 => [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), TrinaryLogic::createYes(), ], 6 => [ @@ -190,7 +199,7 @@ public function dataAccepts(): array TemplateTypeScope::createWithFunction('foo'), 'T', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), )), new ConstantStringType('NonexistentClass'), TrinaryLogic::createNo(), @@ -200,11 +209,11 @@ public function dataAccepts(): array TemplateTypeScope::createWithClass('Foo'), 'T', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), )), new UnionType([ - new ConstantStringType(\DateTime::class), - new ConstantStringType(\Exception::class), + new ConstantStringType(DateTime::class), + new ConstantStringType(Exception::class), ]), TrinaryLogic::createYes(), ], @@ -213,7 +222,7 @@ public function dataAccepts(): array TemplateTypeScope::createWithClass('Foo'), 'T', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), )), new ClassStringType(), TrinaryLogic::createYes(), @@ -223,13 +232,13 @@ public function dataAccepts(): array TemplateTypeScope::createWithClass('Foo'), 'T', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), )), new GenericClassStringType(TemplateTypeFactory::create( TemplateTypeScope::createWithClass('Boo'), 'U', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), )), TrinaryLogic::createMaybe(), ], @@ -238,7 +247,7 @@ public function dataAccepts(): array TemplateTypeScope::createWithClass('Foo'), 'T', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), )), new UnionType([new IntegerType(), new StringType()]), TrinaryLogic::createMaybe(), @@ -248,7 +257,7 @@ public function dataAccepts(): array TemplateTypeScope::createWithClass('Foo'), 'T', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), )), new BenevolentUnionType([new IntegerType(), new StringType()]), TrinaryLogic::createMaybe(), @@ -262,38 +271,40 @@ public function dataAccepts(): array public function testAccepts( GenericClassStringType $acceptingType, Type $acceptedType, - TrinaryLogic $expectedResult + TrinaryLogic $expectedResult, ): void { $actualResult = $acceptingType->accepts($acceptedType, true); $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> accepts(%s)', $acceptingType->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $acceptingType->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())), ); } public function dataEquals(): array { + $reflectionProvider = $this->createReflectionProvider(); + return [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), true, ], [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\stdClass::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(stdClass::class)), false, ], [ - new GenericClassStringType(new StaticType(\Exception::class)), - new GenericClassStringType(new StaticType(\Exception::class)), + new GenericClassStringType(new StaticType($reflectionProvider->getClass(Exception::class))), + new GenericClassStringType(new StaticType($reflectionProvider->getClass(Exception::class))), true, ], [ - new GenericClassStringType(new StaticType(\Exception::class)), - new GenericClassStringType(new StaticType(\stdClass::class)), + new GenericClassStringType(new StaticType($reflectionProvider->getClass(Exception::class))), + new GenericClassStringType(new StaticType($reflectionProvider->getClass(stdClass::class))), false, ], ]; @@ -312,14 +323,14 @@ public function testEquals(GenericClassStringType $type, Type $otherType, bool $ $this->assertSame( $expected, $actual, - sprintf('%s -> equals(%s)', $typeDescription, $otherTypeDescription) + sprintf('%s -> equals(%s)', $typeDescription, $otherTypeDescription), ); $actual = $otherType->equals($type); $this->assertSame( $expected, $actual, - sprintf('%s -> equals(%s)', $otherTypeDescription, $typeDescription) + sprintf('%s -> equals(%s)', $otherTypeDescription, $typeDescription), ); } diff --git a/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php b/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php index fe412c6971..ac6c6e9ad0 100644 --- a/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php +++ b/tests/PHPStan/Type/Generic/GenericObjectTypeTest.php @@ -2,6 +2,11 @@ namespace PHPStan\Type\Generic; +use DateTime; +use DateTimeInterface; +use Exception; +use Iterator; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; @@ -15,8 +20,13 @@ use PHPStan\Type\Type; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; +use ReflectionClass; +use stdClass; +use Traversable; +use function array_map; +use function sprintf; -class GenericObjectTypeTest extends \PHPStan\Testing\TestCase +class GenericObjectTypeTest extends PHPStanTestCase { public function dataIsSuperTypeOf(): array @@ -88,43 +98,43 @@ public function dataIsSuperTypeOf(): array TrinaryLogic::createMaybe(), ], [ - new ObjectType(\ReflectionClass::class), - new GenericObjectType(\ReflectionClass::class, [ - new ObjectType(\stdClass::class), + new ObjectType(ReflectionClass::class), + new GenericObjectType(ReflectionClass::class, [ + new ObjectType(stdClass::class), ]), TrinaryLogic::createYes(), ], [ - new GenericObjectType(\ReflectionClass::class, [ - new ObjectType(\stdClass::class), + new GenericObjectType(ReflectionClass::class, [ + new ObjectType(stdClass::class), ]), - new ObjectType(\ReflectionClass::class), + new ObjectType(ReflectionClass::class), TrinaryLogic::createMaybe(), ], [ - new GenericObjectType(\ReflectionClass::class, [ + new GenericObjectType(ReflectionClass::class, [ new ObjectWithoutClassType(), ]), - new GenericObjectType(\ReflectionClass::class, [ - new ObjectType(\stdClass::class), + new GenericObjectType(ReflectionClass::class, [ + new ObjectType(stdClass::class), ]), TrinaryLogic::createYes(), ], [ - new GenericObjectType(\ReflectionClass::class, [ - new ObjectType(\stdClass::class), + new GenericObjectType(ReflectionClass::class, [ + new ObjectType(stdClass::class), ]), - new GenericObjectType(\ReflectionClass::class, [ + new GenericObjectType(ReflectionClass::class, [ new ObjectWithoutClassType(), ]), TrinaryLogic::createMaybe(), ], [ - new GenericObjectType(\ReflectionClass::class, [ - new ObjectType(\Exception::class), + new GenericObjectType(ReflectionClass::class, [ + new ObjectType(Exception::class), ]), - new GenericObjectType(\ReflectionClass::class, [ - new ObjectType(\stdClass::class), + new GenericObjectType(ReflectionClass::class, [ + new ObjectType(stdClass::class), ]), TrinaryLogic::createNo(), ], @@ -140,7 +150,7 @@ public function testIsSuperTypeOf(Type $type, Type $otherType, TrinaryLogic $exp $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -183,18 +193,18 @@ public function dataAccepts(): array TrinaryLogic::createNo(), ], 'generic object accepts normal object of same type' => [ - new GenericObjectType(\Traversable::class, [new MixedType(true), new ObjectType('DateTimeInteface')]), - new ObjectType(\Traversable::class), + new GenericObjectType(Traversable::class, [new MixedType(true), new ObjectType('DateTimeInteface')]), + new ObjectType(Traversable::class), TrinaryLogic::createYes(), ], [ - new GenericObjectType(\Iterator::class, [new MixedType(true), new MixedType(true)]), - new ObjectType(\Iterator::class), + new GenericObjectType(Iterator::class, [new MixedType(true), new MixedType(true)]), + new ObjectType(Iterator::class), TrinaryLogic::createYes(), ], [ - new GenericObjectType(\Iterator::class, [new MixedType(true), new MixedType(true)]), - new IntersectionType([new ObjectType(\Iterator::class), new ObjectType(\DateTimeInterface::class)]), + new GenericObjectType(Iterator::class, [new MixedType(true), new MixedType(true)]), + new IntersectionType([new ObjectType(Iterator::class), new ObjectType(DateTimeInterface::class)]), TrinaryLogic::createYes(), ], ]; @@ -206,33 +216,31 @@ public function dataAccepts(): array public function testAccepts( Type $acceptingType, Type $acceptedType, - TrinaryLogic $expectedResult + TrinaryLogic $expectedResult, ): void { $actualResult = $acceptingType->accepts($acceptedType, true); $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> accepts(%s)', $acceptingType->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $acceptingType->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())), ); } /** @return array}> */ public function dataInferTemplateTypes(): array { - $templateType = static function (string $name, ?Type $bound = null): Type { - return TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - $name, - $bound ?? new MixedType(), - TemplateTypeVariance::createInvariant() - ); - }; + $templateType = static fn (string $name, ?Type $bound = null): Type => TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + $name, + $bound ?? new MixedType(), + TemplateTypeVariance::createInvariant(), + ); return [ 'simple' => [ new GenericObjectType(A\A::class, [ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), ]), new GenericObjectType(A\A::class, [ $templateType('T'), @@ -241,7 +249,7 @@ public function dataInferTemplateTypes(): array ], 'two types' => [ new GenericObjectType(A\A2::class, [ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), new IntegerType(), ]), new GenericObjectType(A\A2::class, [ @@ -253,12 +261,12 @@ public function dataInferTemplateTypes(): array 'union' => [ new UnionType([ new GenericObjectType(A\A2::class, [ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), new IntegerType(), ]), new GenericObjectType(A\A2::class, [ new IntegerType(), - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), ]), ]), new GenericObjectType(A\A2::class, [ @@ -270,7 +278,7 @@ public function dataInferTemplateTypes(): array 'nested' => [ new GenericObjectType(A\A::class, [ new GenericObjectType(A\A2::class, [ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), new IntegerType(), ]), ]), @@ -284,27 +292,27 @@ public function dataInferTemplateTypes(): array ], 'missing type' => [ new GenericObjectType(A\A2::class, [ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), ]), new GenericObjectType(A\A2::class, [ - $templateType('K', new ObjectType(\DateTimeInterface::class)), - $templateType('V', new ObjectType(\DateTimeInterface::class)), + $templateType('K', new ObjectType(DateTimeInterface::class)), + $templateType('V', new ObjectType(DateTimeInterface::class)), ]), ['K' => 'DateTime'], ], 'wrong class' => [ new GenericObjectType(B\I::class, [ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), ]), new GenericObjectType(A\A::class, [ - $templateType('T', new ObjectType(\DateTimeInterface::class)), + $templateType('T', new ObjectType(DateTimeInterface::class)), ]), [], ], 'wrong type' => [ new IntegerType(), new GenericObjectType(A\A::class, [ - $templateType('T', new ObjectType(\DateTimeInterface::class)), + $templateType('T', new ObjectType(DateTimeInterface::class)), ]), [], ], @@ -328,23 +336,19 @@ public function testResolveTemplateTypes(Type $received, Type $template, array $ $this->assertSame( $expectedTypes, - array_map(static function (Type $type): string { - return $type->describe(VerbosityLevel::precise()); - }, $result->getTypes()) + array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::precise()), $result->getTypes()), ); } /** @return array}> */ public function dataGetReferencedTypeArguments(): array { - $templateType = static function (string $name, ?Type $bound = null): TemplateType { - return TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - $name, - $bound ?? new MixedType(), - TemplateTypeVariance::createInvariant() - ); - }; + $templateType = static fn (string $name, ?Type $bound = null): TemplateType => TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + $name, + $bound ?? new MixedType(), + TemplateTypeVariance::createInvariant(), + ); return [ 'param: Invariant' => [ @@ -355,7 +359,7 @@ public function dataGetReferencedTypeArguments(): array [ new TemplateTypeReference( $templateType('T'), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), ], ], @@ -367,7 +371,7 @@ public function dataGetReferencedTypeArguments(): array [ new TemplateTypeReference( $templateType('T'), - TemplateTypeVariance::createContravariant() + TemplateTypeVariance::createContravariant(), ), ], ], @@ -381,7 +385,7 @@ public function dataGetReferencedTypeArguments(): array [ new TemplateTypeReference( $templateType('T'), - TemplateTypeVariance::createContravariant() + TemplateTypeVariance::createContravariant(), ), ], ], @@ -397,7 +401,7 @@ public function dataGetReferencedTypeArguments(): array [ new TemplateTypeReference( $templateType('T'), - TemplateTypeVariance::createContravariant() + TemplateTypeVariance::createContravariant(), ), ], ], @@ -409,7 +413,7 @@ public function dataGetReferencedTypeArguments(): array [ new TemplateTypeReference( $templateType('T'), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), ], ], @@ -421,7 +425,7 @@ public function dataGetReferencedTypeArguments(): array [ new TemplateTypeReference( $templateType('T'), - TemplateTypeVariance::createCovariant() + TemplateTypeVariance::createCovariant(), ), ], ], @@ -435,7 +439,7 @@ public function dataGetReferencedTypeArguments(): array [ new TemplateTypeReference( $templateType('T'), - TemplateTypeVariance::createCovariant() + TemplateTypeVariance::createCovariant(), ), ], ], @@ -451,7 +455,7 @@ public function dataGetReferencedTypeArguments(): array [ new TemplateTypeReference( $templateType('T'), - TemplateTypeVariance::createCovariant() + TemplateTypeVariance::createCovariant(), ), ], ], @@ -465,7 +469,7 @@ public function dataGetReferencedTypeArguments(): array [ new TemplateTypeReference( $templateType('T'), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), ], ], @@ -484,19 +488,15 @@ public function testGetReferencedTypeArguments(TemplateTypeVariance $positionVar $result[] = $r; } - $comparableResult = array_map(static function (TemplateTypeReference $ref): array { - return [ - 'type' => $ref->getType()->describe(VerbosityLevel::typeOnly()), - 'positionVariance' => $ref->getPositionVariance()->describe(), - ]; - }, $result); + $comparableResult = array_map(static fn (TemplateTypeReference $ref): array => [ + 'type' => $ref->getType()->describe(VerbosityLevel::typeOnly()), + 'positionVariance' => $ref->getPositionVariance()->describe(), + ], $result); - $comparableExpect = array_map(static function (TemplateTypeReference $ref): array { - return [ - 'type' => $ref->getType()->describe(VerbosityLevel::typeOnly()), - 'positionVariance' => $ref->getPositionVariance()->describe(), - ]; - }, $expectedReferences); + $comparableExpect = array_map(static fn (TemplateTypeReference $ref): array => [ + 'type' => $ref->getType()->describe(VerbosityLevel::typeOnly()), + 'positionVariance' => $ref->getPositionVariance()->describe(), + ], $expectedReferences); $this->assertSame($comparableExpect, $comparableResult); } diff --git a/tests/PHPStan/Type/Generic/TemplateTypeHelperTest.php b/tests/PHPStan/Type/Generic/TemplateTypeHelperTest.php index 279617324e..a83addb405 100644 --- a/tests/PHPStan/Type/Generic/TemplateTypeHelperTest.php +++ b/tests/PHPStan/Type/Generic/TemplateTypeHelperTest.php @@ -2,11 +2,13 @@ namespace PHPStan\Type\Generic; +use DateTime; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\IntersectionType; use PHPStan\Type\ObjectType; use PHPStan\Type\VerbosityLevel; -class TemplateTypeHelperTest extends \PHPStan\Testing\TestCase +class TemplateTypeHelperTest extends PHPStanTestCase { public function testIssue2512(): void @@ -15,34 +17,34 @@ public function testIssue2512(): void TemplateTypeScope::createWithFunction('a'), 'T', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ); $type = TemplateTypeHelper::resolveTemplateTypes( $templateType, new TemplateTypeMap([ 'T' => $templateType, - ]) + ]), ); $this->assertEquals( 'T (function a(), parameter)', - $type->describe(VerbosityLevel::precise()) + $type->describe(VerbosityLevel::precise()), ); $type = TemplateTypeHelper::resolveTemplateTypes( $templateType, new TemplateTypeMap([ 'T' => new IntersectionType([ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), $templateType, ]), - ]) + ]), ); $this->assertEquals( 'DateTime&T (function a(), parameter)', - $type->describe(VerbosityLevel::precise()) + $type->describe(VerbosityLevel::precise()), ); } diff --git a/tests/PHPStan/Type/Generic/TemplateTypeMapTest.php b/tests/PHPStan/Type/Generic/TemplateTypeMapTest.php index 77c0c11b13..ff9f5fe58e 100644 --- a/tests/PHPStan/Type/Generic/TemplateTypeMapTest.php +++ b/tests/PHPStan/Type/Generic/TemplateTypeMapTest.php @@ -2,6 +2,8 @@ namespace PHPStan\Type\Generic; +use Exception; +use InvalidArgumentException; use PHPStan\Type\ObjectType; use PHPStan\Type\VerbosityLevel; use PHPUnit\Framework\TestCase; @@ -12,45 +14,51 @@ class TemplateTypeMapTest extends TestCase public function dataUnionWithLowerBoundTypes(): iterable { $map = (new TemplateTypeMap([ - 'T' => new ObjectType(\Exception::class), + 'T' => new ObjectType(Exception::class), ]))->convertToLowerBoundTypes(); + yield [ + $map, + Exception::class, + ]; + yield [ $map->union(new TemplateTypeMap([ - 'T' => new ObjectType(\InvalidArgumentException::class), + 'T' => new ObjectType(InvalidArgumentException::class), ])), - \InvalidArgumentException::class, + InvalidArgumentException::class, ]; yield [ $map->union((new TemplateTypeMap([ - 'T' => new ObjectType(\InvalidArgumentException::class), + 'T' => new ObjectType(InvalidArgumentException::class), ]))->convertToLowerBoundTypes()), - \InvalidArgumentException::class, + InvalidArgumentException::class, ]; yield [ (new TemplateTypeMap([ - 'T' => new ObjectType(\Exception::class), + 'T' => new ObjectType(Exception::class), ], [ - 'T' => new ObjectType(\InvalidArgumentException::class), + 'T' => new ObjectType(InvalidArgumentException::class), ]))->convertToLowerBoundTypes(), - \InvalidArgumentException::class, + InvalidArgumentException::class, ]; yield [ (new TemplateTypeMap([ - 'T' => new ObjectType(\InvalidArgumentException::class), + 'T' => new ObjectType(InvalidArgumentException::class), ], [ - 'T' => new ObjectType(\Exception::class), + 'T' => new ObjectType(Exception::class), ]))->convertToLowerBoundTypes(), - \InvalidArgumentException::class, + InvalidArgumentException::class, ]; } /** @dataProvider dataUnionWithLowerBoundTypes */ public function testUnionWithLowerBoundTypes(TemplateTypeMap $map, string $expectedTDescription): void { + $this->assertFalse($map->isEmpty()); $t = $map->getType('T'); $this->assertNotNull($t); $this->assertSame($expectedTDescription, $t->describe(VerbosityLevel::precise())); diff --git a/tests/PHPStan/Type/Generic/TemplateTypeVarianceTest.php b/tests/PHPStan/Type/Generic/TemplateTypeVarianceTest.php index f63858d142..71a57dda6d 100644 --- a/tests/PHPStan/Type/Generic/TemplateTypeVarianceTest.php +++ b/tests/PHPStan/Type/Generic/TemplateTypeVarianceTest.php @@ -10,6 +10,7 @@ use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use PHPUnit\Framework\TestCase; +use function sprintf; class TemplateTypeVarianceTest extends TestCase { @@ -83,18 +84,18 @@ public function testIsValidVariance( Type $a, Type $b, TrinaryLogic $expected, - TrinaryLogic $expectedInversed + TrinaryLogic $expectedInversed, ): void { $this->assertSame( $expected->describe(), $variance->isValidVariance($a, $b)->describe(), - sprintf('%s->isValidVariance(%s, %s)', $variance->describe(), $a->describe(VerbosityLevel::precise()), $b->describe(VerbosityLevel::precise())) + sprintf('%s->isValidVariance(%s, %s)', $variance->describe(), $a->describe(VerbosityLevel::precise()), $b->describe(VerbosityLevel::precise())), ); $this->assertSame( $expectedInversed->describe(), $variance->isValidVariance($b, $a)->describe(), - sprintf('%s->isValidVariance(%s, %s)', $variance->describe(), $b->describe(VerbosityLevel::precise()), $a->describe(VerbosityLevel::precise())) + sprintf('%s->isValidVariance(%s, %s)', $variance->describe(), $b->describe(VerbosityLevel::precise()), $a->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/IntegerTypeTest.php b/tests/PHPStan/Type/IntegerTypeTest.php index 7b98244725..a1c83f7885 100644 --- a/tests/PHPStan/Type/IntegerTypeTest.php +++ b/tests/PHPStan/Type/IntegerTypeTest.php @@ -2,12 +2,14 @@ namespace PHPStan\Type; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use function sprintf; -class IntegerTypeTest extends \PHPStan\Testing\TestCase +class IntegerTypeTest extends PHPStanTestCase { public function dataAccepts(): array @@ -48,9 +50,6 @@ public function dataAccepts(): array /** * @dataProvider dataAccepts - * @param IntegerType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testAccepts(IntegerType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -58,7 +57,7 @@ public function testAccepts(IntegerType $type, Type $otherType, TrinaryLogic $ex $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -97,9 +96,6 @@ public function dataIsSuperTypeOf(): iterable /** * @dataProvider dataIsSuperTypeOf - * @param IntegerType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(IntegerType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -107,7 +103,7 @@ public function testIsSuperTypeOf(IntegerType $type, Type $otherType, TrinaryLog $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -159,9 +155,6 @@ public function dataEquals(): array /** * @dataProvider dataEquals - * @param IntegerType $type - * @param Type $otherType - * @param bool $expectedResult */ public function testEquals(IntegerType $type, Type $otherType, bool $expectedResult): void { @@ -169,7 +162,7 @@ public function testEquals(IntegerType $type, Type $otherType, bool $expectedRes $this->assertSame( $expectedResult, $actualResult, - sprintf('%s->equals(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s->equals(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/IntersectionTypeTest.php b/tests/PHPStan/Type/IntersectionTypeTest.php index 634c11b0b4..c771c085fd 100644 --- a/tests/PHPStan/Type/IntersectionTypeTest.php +++ b/tests/PHPStan/Type/IntersectionTypeTest.php @@ -2,6 +2,9 @@ namespace PHPStan\Type; +use DoctrineIntersectionTypeIsSupertypeOf\Collection; +use Iterator; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\HasOffsetType; use PHPStan\Type\Accessory\HasPropertyType; @@ -9,12 +12,15 @@ use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use stdClass; use Test\ClassWithToString; +use Traversable; +use function sprintf; -class IntersectionTypeTest extends \PHPStan\Testing\TestCase +class IntersectionTypeTest extends PHPStanTestCase { - public function dataAccepts(): \Iterator + public function dataAccepts(): Iterator { $intersectionType = new IntersectionType([ new ObjectType('Collection'), @@ -57,9 +63,6 @@ public function dataAccepts(): \Iterator /** * @dataProvider dataAccepts - * @param IntersectionType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testAccepts(IntersectionType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -67,7 +70,7 @@ public function testAccepts(IntersectionType $type, Type $otherType, TrinaryLogi $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -78,7 +81,7 @@ public function dataIsCallable(): array new IntersectionType([ new ConstantArrayType( [new ConstantIntegerType(0), new ConstantIntegerType(1)], - [new ConstantStringType('Closure'), new ConstantStringType('bind')] + [new ConstantStringType('Closure'), new ConstantStringType('bind')], ), new IterableType(new MixedType(), new ObjectType('Item')), ]), @@ -103,8 +106,6 @@ public function dataIsCallable(): array /** * @dataProvider dataIsCallable - * @param IntersectionType $type - * @param TrinaryLogic $expectedResult */ public function testIsCallable(IntersectionType $type, TrinaryLogic $expectedResult): void { @@ -112,11 +113,11 @@ public function testIsCallable(IntersectionType $type, TrinaryLogic $expectedRes $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())), ); } - public function dataIsSuperTypeOf(): \Iterator + public function dataIsSuperTypeOf(): Iterator { $intersectionTypeA = new IntersectionType([ new ObjectType('ArrayObject'), @@ -147,16 +148,6 @@ public function dataIsSuperTypeOf(): \Iterator TrinaryLogic::createNo(), ]; - $intersectionTypeB = new IntersectionType([ - new IntegerType(), - ]); - - yield [ - $intersectionTypeB, - $intersectionTypeB, - TrinaryLogic::createYes(), - ]; - yield [ new IntersectionType([ new ArrayType(new MixedType(), new MixedType()), @@ -205,24 +196,24 @@ public function dataIsSuperTypeOf(): \Iterator yield [ new IntersectionType([ - new ObjectType(\Traversable::class), - new IterableType(new MixedType(true), new ObjectType(\stdClass::class)), + new ObjectType(Traversable::class), + new IterableType(new MixedType(true), new ObjectType(stdClass::class)), ]), new IntersectionType([ - new ObjectType(\Traversable::class), - new IterableType(new MixedType(true), new ObjectType(\stdClass::class)), + new ObjectType(Traversable::class), + new IterableType(new MixedType(true), new ObjectType(stdClass::class)), ]), TrinaryLogic::createYes(), ]; yield [ new IntersectionType([ - new ObjectType(\DoctrineIntersectionTypeIsSupertypeOf\Collection::class), - new IterableType(new MixedType(true), new ObjectType(\stdClass::class)), + new ObjectType(Collection::class), + new IterableType(new MixedType(true), new ObjectType(stdClass::class)), ]), new IntersectionType([ - new ObjectType(\DoctrineIntersectionTypeIsSupertypeOf\Collection::class), - new IterableType(new MixedType(true), new ObjectType(\stdClass::class)), + new ObjectType(Collection::class), + new IterableType(new MixedType(true), new ObjectType(stdClass::class)), ]), TrinaryLogic::createYes(), ]; @@ -230,47 +221,47 @@ public function dataIsSuperTypeOf(): \Iterator yield [ new IntersectionType([ new ObjectType(\TestIntersectionTypeIsSupertypeOf\Collection::class), - new IterableType(new MixedType(true), new ObjectType(\stdClass::class)), + new IterableType(new MixedType(true), new ObjectType(stdClass::class)), ]), new IntersectionType([ new ObjectType(\TestIntersectionTypeIsSupertypeOf\Collection::class), - new IterableType(new MixedType(true), new ObjectType(\stdClass::class)), + new IterableType(new MixedType(true), new ObjectType(stdClass::class)), ]), TrinaryLogic::createYes(), ]; yield [ new IntersectionType([ - new ObjectType(\DoctrineIntersectionTypeIsSupertypeOf\Collection::class), - new IterableType(new MixedType(true), new ObjectType(\stdClass::class)), + new ObjectType(Collection::class), + new IterableType(new MixedType(true), new ObjectType(stdClass::class)), ]), new IntersectionType([ - new ObjectType(\DoctrineIntersectionTypeIsSupertypeOf\Collection::class), - new IterableType(new MixedType(), new ObjectType(\stdClass::class)), + new ObjectType(Collection::class), + new IterableType(new MixedType(), new ObjectType(stdClass::class)), ]), TrinaryLogic::createYes(), ]; yield [ new IntersectionType([ - new ObjectType(\DoctrineIntersectionTypeIsSupertypeOf\Collection::class), - new IterableType(new MixedType(), new ObjectType(\stdClass::class)), + new ObjectType(Collection::class), + new IterableType(new MixedType(), new ObjectType(stdClass::class)), ]), new IntersectionType([ - new ObjectType(\DoctrineIntersectionTypeIsSupertypeOf\Collection::class), - new IterableType(new MixedType(true), new ObjectType(\stdClass::class)), + new ObjectType(Collection::class), + new IterableType(new MixedType(true), new ObjectType(stdClass::class)), ]), TrinaryLogic::createYes(), ]; yield [ new IntersectionType([ - new ObjectType(\DoctrineIntersectionTypeIsSupertypeOf\Collection::class), - new IterableType(new MixedType(), new ObjectType(\stdClass::class)), + new ObjectType(Collection::class), + new IterableType(new MixedType(), new ObjectType(stdClass::class)), ]), new IntersectionType([ - new ObjectType(\DoctrineIntersectionTypeIsSupertypeOf\Collection::class), - new IterableType(new MixedType(), new ObjectType(\stdClass::class)), + new ObjectType(Collection::class), + new IterableType(new MixedType(), new ObjectType(stdClass::class)), ]), TrinaryLogic::createYes(), ]; @@ -278,9 +269,6 @@ public function dataIsSuperTypeOf(): \Iterator /** * @dataProvider dataIsSuperTypeOf - * @param IntersectionType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(IntersectionType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -288,11 +276,11 @@ public function testIsSuperTypeOf(IntersectionType $type, Type $otherType, Trina $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } - public function dataIsSubTypeOf(): \Iterator + public function dataIsSubTypeOf(): Iterator { $intersectionTypeA = new IntersectionType([ new ObjectType('ArrayObject'), @@ -335,16 +323,6 @@ public function dataIsSubTypeOf(): \Iterator TrinaryLogic::createNo(), ]; - $intersectionTypeB = new IntersectionType([ - new IntegerType(), - ]); - - yield [ - $intersectionTypeB, - $intersectionTypeB, - TrinaryLogic::createYes(), - ]; - $intersectionTypeC = new IntersectionType([ new StringType(), new CallableType(), @@ -391,9 +369,6 @@ public function dataIsSubTypeOf(): \Iterator /** * @dataProvider dataIsSubTypeOf - * @param IntersectionType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSubTypeOf(IntersectionType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -401,15 +376,12 @@ public function testIsSubTypeOf(IntersectionType $type, Type $otherType, Trinary $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSubTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSubTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } /** * @dataProvider dataIsSubTypeOf - * @param IntersectionType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSubTypeOfInversed(IntersectionType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -417,14 +389,14 @@ public function testIsSubTypeOfInversed(IntersectionType $type, Type $otherType, $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())), ); } public function testToBooleanCrash(): void { $type = new IntersectionType([new NeverType(), new NonEmptyArrayType()]); - $this->assertSame('bool', $type->toBoolean()->describe(VerbosityLevel::precise())); + $this->assertSame('true', $type->toBoolean()->describe(VerbosityLevel::precise())); } } diff --git a/tests/PHPStan/Type/IterableTypeTest.php b/tests/PHPStan/Type/IterableTypeTest.php index 7aaf86149c..02a10d0dde 100644 --- a/tests/PHPStan/Type/IterableTypeTest.php +++ b/tests/PHPStan/Type/IterableTypeTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Type; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\HasMethodType; use PHPStan\Type\Accessory\HasPropertyType; @@ -10,8 +11,10 @@ use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; +use function array_map; +use function sprintf; -class IterableTypeTest extends \PHPStan\Testing\TestCase +class IterableTypeTest extends PHPStanTestCase { public function dataIsSuperTypeOf(): array @@ -57,9 +60,6 @@ public function dataIsSuperTypeOf(): array /** * @dataProvider dataIsSuperTypeOf - * @param IterableType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(IterableType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -67,7 +67,7 @@ public function testIsSuperTypeOf(IterableType $type, Type $otherType, TrinaryLo $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -159,9 +159,6 @@ public function dataIsSubTypeOf(): array /** * @dataProvider dataIsSubTypeOf - * @param IterableType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSubTypeOf(IterableType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -169,15 +166,12 @@ public function testIsSubTypeOf(IterableType $type, Type $otherType, TrinaryLogi $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSubTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSubTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } /** * @dataProvider dataIsSubTypeOf - * @param IterableType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSubTypeOfInversed(IterableType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -185,41 +179,39 @@ public function testIsSubTypeOfInversed(IterableType $type, Type $otherType, Tri $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())), ); } public function dataInferTemplateTypes(): array { - $templateType = static function (string $name): Type { - return TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction('a'), - $name, - new MixedType(), - TemplateTypeVariance::createInvariant() - ); - }; + $templateType = static fn (string $name): Type => TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + $name, + new MixedType(), + TemplateTypeVariance::createInvariant(), + ); return [ 'receive iterable' => [ new IterableType( new MixedType(), - new ObjectType('DateTime') + new ObjectType('DateTime'), ), new IterableType( new MixedType(), - $templateType('T') + $templateType('T'), ), ['T' => 'DateTime'], ], 'receive iterable template key' => [ new IterableType( new StringType(), - new ObjectType('DateTime') + new ObjectType('DateTime'), ), new IterableType( $templateType('U'), - $templateType('T') + $templateType('T'), ), ['U' => 'string', 'T' => 'DateTime'], ], @@ -227,7 +219,7 @@ public function dataInferTemplateTypes(): array new MixedType(), new IterableType( new MixedType(), - $templateType('T') + $templateType('T'), ), [], ], @@ -235,7 +227,7 @@ public function dataInferTemplateTypes(): array new StringType(), new IterableType( new MixedType(), - $templateType('T') + $templateType('T'), ), [], ], @@ -252,9 +244,7 @@ public function testResolveTemplateTypes(Type $received, Type $template, array $ $this->assertSame( $expectedTypes, - array_map(static function (Type $type): string { - return $type->describe(VerbosityLevel::precise()); - }, $result->getTypes()) + array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::precise()), $result->getTypes()), ); } @@ -264,7 +254,7 @@ public function dataDescribe(): array TemplateTypeScope::createWithFunction('a'), 'T', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ); return [ @@ -312,13 +302,13 @@ public function dataAccepts(): array TemplateTypeScope::createWithFunction('foo'), 'T', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ); return [ [ new IterableType( new MixedType(), - $t + $t, ), new ConstantArrayType([], []), TrinaryLogic::createYes(), @@ -326,7 +316,7 @@ public function dataAccepts(): array [ new IterableType( new MixedType(), - $t->toArgument() + $t->toArgument(), ), new ConstantArrayType([], []), TrinaryLogic::createYes(), @@ -336,9 +326,6 @@ public function dataAccepts(): array /** * @dataProvider dataAccepts - * @param IterableType $iterableType - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testAccepts(IterableType $iterableType, Type $otherType, TrinaryLogic $expectedResult): void { @@ -346,7 +333,7 @@ public function testAccepts(IterableType $iterableType, Type $otherType, Trinary $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> accepts(%s)', $iterableType->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $iterableType->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/MixedTypeTest.php b/tests/PHPStan/Type/MixedTypeTest.php index acd24ecad6..e639b15708 100644 --- a/tests/PHPStan/Type/MixedTypeTest.php +++ b/tests/PHPStan/Type/MixedTypeTest.php @@ -2,10 +2,12 @@ namespace PHPStan\Type; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantIntegerType; +use function sprintf; -class MixedTypeTest extends \PHPStan\Testing\TestCase +class MixedTypeTest extends PHPStanTestCase { public function dataIsSuperTypeOf(): array @@ -141,14 +143,16 @@ public function dataIsSuperTypeOf(): array new UnionType([new StringType(), new IntegerType()]), TrinaryLogic::createYes(), ], + 26 => [ + new MixedType(), + new StrictMixedType(), + TrinaryLogic::createYes(), + ], ]; } /** * @dataProvider dataIsSuperTypeOf - * @param \PHPStan\Type\MixedType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(MixedType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -156,7 +160,7 @@ public function testIsSuperTypeOf(MixedType $type, Type $otherType, TrinaryLogic $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/ObjectTypeTest.php b/tests/PHPStan/Type/ObjectTypeTest.php index 848e97b029..81298886b5 100644 --- a/tests/PHPStan/Type/ObjectTypeTest.php +++ b/tests/PHPStan/Type/ObjectTypeTest.php @@ -2,6 +2,19 @@ namespace PHPStan\Type; +use ArrayAccess; +use ArrayObject; +use Closure; +use Countable; +use DateInterval; +use DateTime; +use DateTimeImmutable; +use DateTimeInterface; +use Generator; +use InvalidArgumentException; +use Iterator; +use LogicException; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\HasMethodType; use PHPStan\Type\Accessory\HasPropertyType; @@ -10,8 +23,13 @@ use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; +use SimpleXMLElement; +use stdClass; +use Throwable; +use Traversable; +use function sprintf; -class ObjectTypeTest extends \PHPStan\Testing\TestCase +class ObjectTypeTest extends PHPStanTestCase { public function dataIsIterable(): array @@ -26,8 +44,6 @@ public function dataIsIterable(): array /** * @dataProvider dataIsIterable - * @param ObjectType $type - * @param TrinaryLogic $expectedResult */ public function testIsIterable(ObjectType $type, TrinaryLogic $expectedResult): void { @@ -35,7 +51,7 @@ public function testIsIterable(ObjectType $type, TrinaryLogic $expectedResult): $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isIterable()', $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isIterable()', $type->describe(VerbosityLevel::precise())), ); } @@ -50,8 +66,6 @@ public function dataIsCallable(): array /** * @dataProvider dataIsCallable - * @param ObjectType $type - * @param TrinaryLogic $expectedResult */ public function testIsCallable(ObjectType $type, TrinaryLogic $expectedResult): void { @@ -59,12 +73,14 @@ public function testIsCallable(ObjectType $type, TrinaryLogic $expectedResult): $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())), ); } public function dataIsSuperTypeOf(): array { + $reflectionProvider = $this->createReflectionProvider(); + return [ 0 => [ new ObjectType('UnknownClassA'), @@ -72,164 +88,164 @@ public function dataIsSuperTypeOf(): array TrinaryLogic::createMaybe(), ], 1 => [ - new ObjectType(\ArrayAccess::class), - new ObjectType(\Traversable::class), + new ObjectType(ArrayAccess::class), + new ObjectType(Traversable::class), TrinaryLogic::createMaybe(), ], 2 => [ - new ObjectType(\Countable::class), - new ObjectType(\Countable::class), + new ObjectType(Countable::class), + new ObjectType(Countable::class), TrinaryLogic::createYes(), ], 3 => [ - new ObjectType(\DateTimeImmutable::class), - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), TrinaryLogic::createYes(), ], 4 => [ - new ObjectType(\Traversable::class), - new ObjectType(\ArrayObject::class), + new ObjectType(Traversable::class), + new ObjectType(ArrayObject::class), TrinaryLogic::createYes(), ], 5 => [ - new ObjectType(\Traversable::class), - new ObjectType(\Iterator::class), + new ObjectType(Traversable::class), + new ObjectType(Iterator::class), TrinaryLogic::createYes(), ], 6 => [ - new ObjectType(\ArrayObject::class), - new ObjectType(\Traversable::class), + new ObjectType(ArrayObject::class), + new ObjectType(Traversable::class), TrinaryLogic::createMaybe(), ], 7 => [ - new ObjectType(\Iterator::class), - new ObjectType(\Traversable::class), + new ObjectType(Iterator::class), + new ObjectType(Traversable::class), TrinaryLogic::createMaybe(), ], 8 => [ - new ObjectType(\ArrayObject::class), - new ObjectType(\DateTimeImmutable::class), + new ObjectType(ArrayObject::class), + new ObjectType(DateTimeImmutable::class), TrinaryLogic::createNo(), ], 9 => [ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new UnionType([ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new StringType(), ]), TrinaryLogic::createMaybe(), ], 10 => [ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new UnionType([ - new ObjectType(\ArrayObject::class), + new ObjectType(ArrayObject::class), new StringType(), ]), TrinaryLogic::createNo(), ], 11 => [ - new ObjectType(\LogicException::class), - new ObjectType(\InvalidArgumentException::class), + new ObjectType(LogicException::class), + new ObjectType(InvalidArgumentException::class), TrinaryLogic::createYes(), ], 12 => [ - new ObjectType(\InvalidArgumentException::class), - new ObjectType(\LogicException::class), + new ObjectType(InvalidArgumentException::class), + new ObjectType(LogicException::class), TrinaryLogic::createMaybe(), ], 13 => [ - new ObjectType(\ArrayAccess::class), - new StaticType(\Traversable::class), + new ObjectType(ArrayAccess::class), + new StaticType($reflectionProvider->getClass(Traversable::class)), TrinaryLogic::createMaybe(), ], 14 => [ - new ObjectType(\Countable::class), - new StaticType(\Countable::class), + new ObjectType(Countable::class), + new StaticType($reflectionProvider->getClass(Countable::class)), TrinaryLogic::createYes(), ], 15 => [ - new ObjectType(\DateTimeImmutable::class), - new StaticType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), TrinaryLogic::createYes(), ], 16 => [ - new ObjectType(\Traversable::class), - new StaticType(\ArrayObject::class), + new ObjectType(Traversable::class), + new StaticType($reflectionProvider->getClass(ArrayObject::class)), TrinaryLogic::createYes(), ], 17 => [ - new ObjectType(\Traversable::class), - new StaticType(\Iterator::class), + new ObjectType(Traversable::class), + new StaticType($reflectionProvider->getClass(Iterator::class)), TrinaryLogic::createYes(), ], 18 => [ - new ObjectType(\ArrayObject::class), - new StaticType(\Traversable::class), + new ObjectType(ArrayObject::class), + new StaticType($reflectionProvider->getClass(Traversable::class)), TrinaryLogic::createMaybe(), ], 19 => [ - new ObjectType(\Iterator::class), - new StaticType(\Traversable::class), + new ObjectType(Iterator::class), + new StaticType($reflectionProvider->getClass(Traversable::class)), TrinaryLogic::createMaybe(), ], 20 => [ - new ObjectType(\ArrayObject::class), - new StaticType(\DateTimeImmutable::class), + new ObjectType(ArrayObject::class), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), TrinaryLogic::createNo(), ], 21 => [ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new UnionType([ - new StaticType(\DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), new StringType(), ]), TrinaryLogic::createMaybe(), ], 22 => [ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new UnionType([ - new StaticType(\ArrayObject::class), + new StaticType($reflectionProvider->getClass(ArrayObject::class)), new StringType(), ]), TrinaryLogic::createNo(), ], 23 => [ - new ObjectType(\LogicException::class), - new StaticType(\InvalidArgumentException::class), + new ObjectType(LogicException::class), + new StaticType($reflectionProvider->getClass(InvalidArgumentException::class)), TrinaryLogic::createYes(), ], 24 => [ - new ObjectType(\InvalidArgumentException::class), - new StaticType(\LogicException::class), + new ObjectType(InvalidArgumentException::class), + new StaticType($reflectionProvider->getClass(LogicException::class)), TrinaryLogic::createMaybe(), ], 25 => [ - new ObjectType(\stdClass::class), + new ObjectType(stdClass::class), new ClosureType([], new MixedType(), false), TrinaryLogic::createNo(), ], 26 => [ - new ObjectType(\Closure::class), + new ObjectType(Closure::class), new ClosureType([], new MixedType(), false), TrinaryLogic::createYes(), ], 27 => [ - new ObjectType(\Countable::class), + new ObjectType(Countable::class), new IterableType(new MixedType(), new MixedType()), TrinaryLogic::createMaybe(), ], 28 => [ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new HasMethodType('format'), TrinaryLogic::createMaybe(), ], 29 => [ - new ObjectType(\Closure::class), + new ObjectType(Closure::class), new HasMethodType('format'), TrinaryLogic::createNo(), ], 30 => [ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new UnionType([ new HasMethodType('format'), new HasMethodType('getTimestamp'), @@ -237,17 +253,17 @@ public function dataIsSuperTypeOf(): array TrinaryLogic::createMaybe(), ], 31 => [ - new ObjectType(\DateInterval::class), + new ObjectType(DateInterval::class), new HasPropertyType('d'), TrinaryLogic::createMaybe(), ], 32 => [ - new ObjectType(\Closure::class), + new ObjectType(Closure::class), new HasPropertyType('d'), TrinaryLogic::createNo(), ], 33 => [ - new ObjectType(\DateInterval::class), + new ObjectType(DateInterval::class), new UnionType([ new HasPropertyType('d'), new HasPropertyType('m'), @@ -266,81 +282,81 @@ public function dataIsSuperTypeOf(): array ], 36 => [ new ObjectType('Exception'), - new ObjectWithoutClassType(new ObjectType(\InvalidArgumentException::class)), + new ObjectWithoutClassType(new ObjectType(InvalidArgumentException::class)), TrinaryLogic::createMaybe(), ], 37 => [ - new ObjectType(\InvalidArgumentException::class), + new ObjectType(InvalidArgumentException::class), new ObjectWithoutClassType(new ObjectType('Exception')), TrinaryLogic::createNo(), ], 38 => [ - new ObjectType(\Throwable::class, new ObjectType(\InvalidArgumentException::class)), - new ObjectType(\InvalidArgumentException::class), + new ObjectType(Throwable::class, new ObjectType(InvalidArgumentException::class)), + new ObjectType(InvalidArgumentException::class), TrinaryLogic::createNo(), ], 39 => [ - new ObjectType(\Throwable::class, new ObjectType(\InvalidArgumentException::class)), + new ObjectType(Throwable::class, new ObjectType(InvalidArgumentException::class)), new ObjectType('Exception'), TrinaryLogic::createYes(), ], 40 => [ - new ObjectType(\Throwable::class, new ObjectType('Exception')), - new ObjectType(\InvalidArgumentException::class), + new ObjectType(Throwable::class, new ObjectType('Exception')), + new ObjectType(InvalidArgumentException::class), TrinaryLogic::createNo(), ], 41 => [ - new ObjectType(\Throwable::class, new ObjectType('Exception')), + new ObjectType(Throwable::class, new ObjectType('Exception')), new ObjectType('Exception'), TrinaryLogic::createNo(), ], 42 => [ - new ObjectType(\Throwable::class, new ObjectType('Exception')), - new ObjectType(\Throwable::class), + new ObjectType(Throwable::class, new ObjectType('Exception')), + new ObjectType(Throwable::class), TrinaryLogic::createYes(), ], 43 => [ - new ObjectType(\Throwable::class), - new ObjectType(\Throwable::class, new ObjectType('Exception')), + new ObjectType(Throwable::class), + new ObjectType(Throwable::class, new ObjectType('Exception')), TrinaryLogic::createYes(), ], 44 => [ - new ObjectType(\Throwable::class), - new ObjectType(\Throwable::class, new ObjectType('Exception')), + new ObjectType(Throwable::class), + new ObjectType(Throwable::class, new ObjectType('Exception')), TrinaryLogic::createYes(), ], 45 => [ new ObjectType('Exception'), - new ObjectType(\Throwable::class, new ObjectType('Exception')), + new ObjectType(Throwable::class, new ObjectType('Exception')), TrinaryLogic::createNo(), ], 46 => [ - new ObjectType(\DateTimeInterface::class), + new ObjectType(DateTimeInterface::class), TemplateTypeFactory::create( - TemplateTypeScope::createWithClass(\DateTimeInterface::class), + TemplateTypeScope::createWithClass(DateTimeInterface::class), 'T', - new ObjectType(\DateTimeInterface::class), - TemplateTypeVariance::createInvariant() + new ObjectType(DateTimeInterface::class), + TemplateTypeVariance::createInvariant(), ), TrinaryLogic::createYes(), ], 47 => [ - new ObjectType(\DateTimeInterface::class), + new ObjectType(DateTimeInterface::class), TemplateTypeFactory::create( - TemplateTypeScope::createWithClass(\DateTime::class), + TemplateTypeScope::createWithClass(DateTime::class), 'T', - new ObjectType(\DateTime::class), - TemplateTypeVariance::createInvariant() + new ObjectType(DateTime::class), + TemplateTypeVariance::createInvariant(), ), TrinaryLogic::createYes(), ], 48 => [ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), TemplateTypeFactory::create( - TemplateTypeScope::createWithClass(\DateTimeInterface::class), + TemplateTypeScope::createWithClass(DateTimeInterface::class), 'T', - new ObjectType(\DateTimeInterface::class), - TemplateTypeVariance::createInvariant() + new ObjectType(DateTimeInterface::class), + TemplateTypeVariance::createInvariant(), ), TrinaryLogic::createMaybe(), ], @@ -349,9 +365,6 @@ public function dataIsSuperTypeOf(): array /** * @dataProvider dataIsSuperTypeOf - * @param ObjectType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(ObjectType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -359,7 +372,7 @@ public function testIsSuperTypeOf(ObjectType $type, Type $otherType, TrinaryLogi $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -367,65 +380,62 @@ public function dataAccepts(): array { return [ [ - new ObjectType(\SimpleXMLElement::class), + new ObjectType(SimpleXMLElement::class), new IntegerType(), TrinaryLogic::createNo(), ], [ - new ObjectType(\SimpleXMLElement::class), + new ObjectType(SimpleXMLElement::class), new ConstantStringType('foo'), TrinaryLogic::createNo(), ], [ - new ObjectType(\Traversable::class), - new GenericObjectType(\Traversable::class, [new MixedType(true), new ObjectType('DateTimeInteface')]), + new ObjectType(Traversable::class), + new GenericObjectType(Traversable::class, [new MixedType(true), new ObjectType('DateTimeInteface')]), TrinaryLogic::createYes(), ], [ - new ObjectType(\DateTimeInterface::class), + new ObjectType(DateTimeInterface::class), TemplateTypeFactory::create( - TemplateTypeScope::createWithClass(\DateTimeInterface::class), + TemplateTypeScope::createWithClass(DateTimeInterface::class), 'T', - new ObjectType(\DateTimeInterface::class), - TemplateTypeVariance::createInvariant() + new ObjectType(DateTimeInterface::class), + TemplateTypeVariance::createInvariant(), ), TrinaryLogic::createYes(), ], [ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), TemplateTypeFactory::create( - TemplateTypeScope::createWithClass(\DateTimeInterface::class), + TemplateTypeScope::createWithClass(DateTimeInterface::class), 'T', - new ObjectType(\DateTimeInterface::class), - TemplateTypeVariance::createInvariant() + new ObjectType(DateTimeInterface::class), + TemplateTypeVariance::createInvariant(), ), - TrinaryLogic::createMaybe(), + TrinaryLogic::createNo(), ], ]; } /** * @dataProvider dataAccepts - * @param \PHPStan\Type\ObjectType $type - * @param Type $acceptedType - * @param TrinaryLogic $expectedResult */ public function testAccepts( ObjectType $type, Type $acceptedType, - TrinaryLogic $expectedResult + TrinaryLogic $expectedResult, ): void { $this->assertSame( $expectedResult->describe(), $type->accepts($acceptedType, true)->describe(), - sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())), ); } public function testGetClassReflectionOfGenericClass(): void { - $objectType = new ObjectType(\Traversable::class); + $objectType = new ObjectType(Traversable::class); $classReflection = $objectType->getClassReflection(); $this->assertNotNull($classReflection); $this->assertSame('Traversable', $classReflection->getDisplayName()); @@ -435,53 +445,53 @@ public function dataHasOffsetValueType(): array { return [ [ - new ObjectType(\stdClass::class), + new ObjectType(stdClass::class), new IntegerType(), TrinaryLogic::createMaybe(), ], [ - new ObjectType(\Generator::class), + new ObjectType(Generator::class), new IntegerType(), TrinaryLogic::createNo(), ], [ - new ObjectType(\ArrayAccess::class), + new ObjectType(ArrayAccess::class), new IntegerType(), TrinaryLogic::createMaybe(), ], [ - new ObjectType(\Countable::class), + new ObjectType(Countable::class), new IntegerType(), TrinaryLogic::createMaybe(), ], [ - new GenericObjectType(\ArrayAccess::class, [new IntegerType(), new MixedType()]), + new GenericObjectType(ArrayAccess::class, [new IntegerType(), new MixedType()]), new IntegerType(), TrinaryLogic::createMaybe(), ], [ - new GenericObjectType(\ArrayAccess::class, [new IntegerType(), new MixedType()]), + new GenericObjectType(ArrayAccess::class, [new IntegerType(), new MixedType()]), new MixedType(), TrinaryLogic::createMaybe(), ], [ - new GenericObjectType(\ArrayAccess::class, [new IntegerType(), new MixedType()]), + new GenericObjectType(ArrayAccess::class, [new IntegerType(), new MixedType()]), new StringType(), TrinaryLogic::createNo(), ], [ - new GenericObjectType(\ArrayAccess::class, [new ObjectType(\DateTimeInterface::class), new MixedType()]), - new ObjectType(\DateTime::class), + new GenericObjectType(ArrayAccess::class, [new ObjectType(DateTimeInterface::class), new MixedType()]), + new ObjectType(DateTime::class), TrinaryLogic::createMaybe(), ], [ - new GenericObjectType(\ArrayAccess::class, [new ObjectType(\DateTime::class), new MixedType()]), - new ObjectType(\DateTimeInterface::class), + new GenericObjectType(ArrayAccess::class, [new ObjectType(DateTime::class), new MixedType()]), + new ObjectType(DateTimeInterface::class), TrinaryLogic::createMaybe(), ], [ - new GenericObjectType(\ArrayAccess::class, [new ObjectType(\DateTime::class), new MixedType()]), - new ObjectType(\stdClass::class), + new GenericObjectType(ArrayAccess::class, [new ObjectType(DateTime::class), new MixedType()]), + new ObjectType(stdClass::class), TrinaryLogic::createNo(), ], ]; @@ -489,20 +499,17 @@ public function dataHasOffsetValueType(): array /** * @dataProvider dataHasOffsetValueType - * @param \PHPStan\Type\ObjectType $type - * @param Type $offsetType - * @param TrinaryLogic $expectedResult */ public function testHasOffsetValueType( ObjectType $type, Type $offsetType, - TrinaryLogic $expectedResult + TrinaryLogic $expectedResult, ): void { $this->assertSame( $expectedResult->describe(), $type->hasOffsetValueType($offsetType)->describe(), - sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $offsetType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $offsetType->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/ObjectWithoutClassTypeTest.php b/tests/PHPStan/Type/ObjectWithoutClassTypeTest.php index 12a2c179ae..c5fde900ce 100644 --- a/tests/PHPStan/Type/ObjectWithoutClassTypeTest.php +++ b/tests/PHPStan/Type/ObjectWithoutClassTypeTest.php @@ -2,9 +2,12 @@ namespace PHPStan\Type; +use InvalidArgumentException; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use function sprintf; -class ObjectWithoutClassTypeTest extends \PHPStan\Testing\TestCase +class ObjectWithoutClassTypeTest extends PHPStanTestCase { public function dataIsSuperTypeOf(): array @@ -26,13 +29,13 @@ public function dataIsSuperTypeOf(): array TrinaryLogic::createNo(), ], [ - new ObjectWithoutClassType(new ObjectType(\InvalidArgumentException::class)), + new ObjectWithoutClassType(new ObjectType(InvalidArgumentException::class)), new ObjectType('Exception'), TrinaryLogic::createMaybe(), ], [ new ObjectWithoutClassType(new ObjectType('Exception')), - new ObjectType(\InvalidArgumentException::class), + new ObjectType(InvalidArgumentException::class), TrinaryLogic::createNo(), ], [ @@ -46,13 +49,13 @@ public function dataIsSuperTypeOf(): array TrinaryLogic::createMaybe(), ], [ - new ObjectWithoutClassType(new ObjectType(\InvalidArgumentException::class)), + new ObjectWithoutClassType(new ObjectType(InvalidArgumentException::class)), new ObjectWithoutClassType(new ObjectType('Exception')), TrinaryLogic::createYes(), ], [ new ObjectWithoutClassType(new ObjectType('Exception')), - new ObjectWithoutClassType(new ObjectType(\InvalidArgumentException::class)), + new ObjectWithoutClassType(new ObjectType(InvalidArgumentException::class)), TrinaryLogic::createMaybe(), ], ]; @@ -60,9 +63,6 @@ public function dataIsSuperTypeOf(): array /** * @dataProvider dataIsSuperTypeOf - * @param ObjectWithoutClassType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(ObjectWithoutClassType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -70,7 +70,7 @@ public function testIsSuperTypeOf(ObjectWithoutClassType $type, Type $otherType, $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/StaticTypeTest.php b/tests/PHPStan/Type/StaticTypeTest.php index 993b9c74c2..670d42aaf7 100644 --- a/tests/PHPStan/Type/StaticTypeTest.php +++ b/tests/PHPStan/Type/StaticTypeTest.php @@ -2,29 +2,39 @@ namespace PHPStan\Type; -use PHPStan\Broker\Broker; +use ArrayAccess; +use ArrayObject; +use Countable; +use DateTimeImmutable; +use Exception; +use InvalidArgumentException; +use Iterator; +use LogicException; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use StaticTypeTest\Base; use StaticTypeTest\Child; use StaticTypeTest\FinalChild; +use stdClass; +use Traversable; +use function sprintf; -class StaticTypeTest extends \PHPStan\Testing\TestCase +class StaticTypeTest extends PHPStanTestCase { public function dataIsIterable(): array { + $reflectionProvider = $this->createReflectionProvider(); + return [ - [new StaticType('ArrayObject'), TrinaryLogic::createYes()], - [new StaticType('Traversable'), TrinaryLogic::createYes()], - [new StaticType('Unknown'), TrinaryLogic::createMaybe()], - [new StaticType('DateTime'), TrinaryLogic::createNo()], + [new StaticType($reflectionProvider->getClass('ArrayObject')), TrinaryLogic::createYes()], + [new StaticType($reflectionProvider->getClass('Traversable')), TrinaryLogic::createYes()], + [new StaticType($reflectionProvider->getClass('DateTime')), TrinaryLogic::createNo()], ]; } /** * @dataProvider dataIsIterable - * @param StaticType $type - * @param TrinaryLogic $expectedResult */ public function testIsIterable(StaticType $type, TrinaryLogic $expectedResult): void { @@ -32,23 +42,22 @@ public function testIsIterable(StaticType $type, TrinaryLogic $expectedResult): $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isIterable()', $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isIterable()', $type->describe(VerbosityLevel::precise())), ); } public function dataIsCallable(): array { + $reflectionProvider = $this->createReflectionProvider(); + return [ - [new StaticType('Closure'), TrinaryLogic::createYes()], - [new StaticType('Unknown'), TrinaryLogic::createMaybe()], - [new StaticType('DateTime'), TrinaryLogic::createMaybe()], + [new StaticType($reflectionProvider->getClass('Closure')), TrinaryLogic::createYes()], + [new StaticType($reflectionProvider->getClass('DateTime')), TrinaryLogic::createMaybe()], ]; } /** * @dataProvider dataIsCallable - * @param StaticType $type - * @param TrinaryLogic $expectedResult */ public function testIsCallable(StaticType $type, TrinaryLogic $expectedResult): void { @@ -56,196 +65,191 @@ public function testIsCallable(StaticType $type, TrinaryLogic $expectedResult): $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())), ); } public function dataIsSuperTypeOf(): array { - $broker = $this->createBroker(); + $reflectionProvider = $this->createReflectionProvider(); return [ - 0 => [ - new StaticType('UnknownClassA'), - new ObjectType('UnknownClassB'), - TrinaryLogic::createMaybe(), - ], 1 => [ - new StaticType(\ArrayAccess::class), - new ObjectType(\Traversable::class), + new StaticType($reflectionProvider->getClass(ArrayAccess::class)), + new ObjectType(Traversable::class), TrinaryLogic::createMaybe(), ], 2 => [ - new StaticType(\Countable::class), - new ObjectType(\Countable::class), + new StaticType($reflectionProvider->getClass(Countable::class)), + new ObjectType(Countable::class), TrinaryLogic::createMaybe(), ], 3 => [ - new StaticType(\DateTimeImmutable::class), - new ObjectType(\DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), + new ObjectType(DateTimeImmutable::class), TrinaryLogic::createMaybe(), ], 4 => [ - new StaticType(\Traversable::class), - new ObjectType(\ArrayObject::class), + new StaticType($reflectionProvider->getClass(Traversable::class)), + new ObjectType(ArrayObject::class), TrinaryLogic::createMaybe(), ], 5 => [ - new StaticType(\Traversable::class), - new ObjectType(\Iterator::class), + new StaticType($reflectionProvider->getClass(Traversable::class)), + new ObjectType(Iterator::class), TrinaryLogic::createMaybe(), ], 6 => [ - new StaticType(\ArrayObject::class), - new ObjectType(\Traversable::class), + new StaticType($reflectionProvider->getClass(ArrayObject::class)), + new ObjectType(Traversable::class), TrinaryLogic::createMaybe(), ], 7 => [ - new StaticType(\Iterator::class), - new ObjectType(\Traversable::class), + new StaticType($reflectionProvider->getClass(Iterator::class)), + new ObjectType(Traversable::class), TrinaryLogic::createMaybe(), ], 8 => [ - new StaticType(\ArrayObject::class), - new ObjectType(\DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(ArrayObject::class)), + new ObjectType(DateTimeImmutable::class), TrinaryLogic::createNo(), ], 9 => [ - new StaticType(\DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), new UnionType([ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new StringType(), ]), TrinaryLogic::createMaybe(), ], 10 => [ - new StaticType(\DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), new UnionType([ - new ObjectType(\ArrayObject::class), + new ObjectType(ArrayObject::class), new StringType(), ]), TrinaryLogic::createNo(), ], 11 => [ - new StaticType(\LogicException::class), - new ObjectType(\InvalidArgumentException::class), + new StaticType($reflectionProvider->getClass(LogicException::class)), + new ObjectType(InvalidArgumentException::class), TrinaryLogic::createMaybe(), ], 12 => [ - new StaticType(\InvalidArgumentException::class), - new ObjectType(\LogicException::class), + new StaticType($reflectionProvider->getClass(InvalidArgumentException::class)), + new ObjectType(LogicException::class), TrinaryLogic::createMaybe(), ], 13 => [ - new StaticType(\ArrayAccess::class), - new StaticType(\Traversable::class), + new StaticType($reflectionProvider->getClass(ArrayAccess::class)), + new StaticType($reflectionProvider->getClass(Traversable::class)), TrinaryLogic::createMaybe(), ], 14 => [ - new StaticType(\Countable::class), - new StaticType(\Countable::class), + new StaticType($reflectionProvider->getClass(Countable::class)), + new StaticType($reflectionProvider->getClass(Countable::class)), TrinaryLogic::createYes(), ], 15 => [ - new StaticType(\DateTimeImmutable::class), - new StaticType(\DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), TrinaryLogic::createYes(), ], 16 => [ - new StaticType(\Traversable::class), - new StaticType(\ArrayObject::class), + new StaticType($reflectionProvider->getClass(Traversable::class)), + new StaticType($reflectionProvider->getClass(ArrayObject::class)), TrinaryLogic::createYes(), ], 17 => [ - new StaticType(\Traversable::class), - new StaticType(\Iterator::class), + new StaticType($reflectionProvider->getClass(Traversable::class)), + new StaticType($reflectionProvider->getClass(Iterator::class)), TrinaryLogic::createYes(), ], 18 => [ - new StaticType(\ArrayObject::class), - new StaticType(\Traversable::class), + new StaticType($reflectionProvider->getClass(ArrayObject::class)), + new StaticType($reflectionProvider->getClass(Traversable::class)), TrinaryLogic::createMaybe(), ], 19 => [ - new StaticType(\Iterator::class), - new StaticType(\Traversable::class), + new StaticType($reflectionProvider->getClass(Iterator::class)), + new StaticType($reflectionProvider->getClass(Traversable::class)), TrinaryLogic::createMaybe(), ], 20 => [ - new StaticType(\ArrayObject::class), - new StaticType(\DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(ArrayObject::class)), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), TrinaryLogic::createNo(), ], 21 => [ - new StaticType(\DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), new UnionType([ - new StaticType(\DateTimeImmutable::class), - new StaticType(\DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), ]), TrinaryLogic::createYes(), ], 22 => [ - new StaticType(\DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), new UnionType([ - new StaticType(\DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), new StringType(), ]), TrinaryLogic::createMaybe(), ], 23 => [ - new StaticType(\DateTimeImmutable::class), + new StaticType($reflectionProvider->getClass(DateTimeImmutable::class)), new UnionType([ - new StaticType(\ArrayObject::class), + new StaticType($reflectionProvider->getClass(ArrayObject::class)), new StringType(), ]), TrinaryLogic::createNo(), ], 24 => [ - new StaticType(\LogicException::class), - new StaticType(\InvalidArgumentException::class), + new StaticType($reflectionProvider->getClass(LogicException::class)), + new StaticType($reflectionProvider->getClass(InvalidArgumentException::class)), TrinaryLogic::createYes(), ], 25 => [ - new StaticType(\InvalidArgumentException::class), - new StaticType(\LogicException::class), + new StaticType($reflectionProvider->getClass(InvalidArgumentException::class)), + new StaticType($reflectionProvider->getClass(LogicException::class)), TrinaryLogic::createMaybe(), ], 26 => [ - new StaticType(\stdClass::class), + new StaticType($reflectionProvider->getClass(stdClass::class)), new ObjectWithoutClassType(), TrinaryLogic::createMaybe(), ], 27 => [ new ObjectWithoutClassType(), - new StaticType(\stdClass::class), + new StaticType($reflectionProvider->getClass(stdClass::class)), TrinaryLogic::createYes(), ], 28 => [ - new ThisType($broker->getClass(\stdClass::class)), + new ThisType($reflectionProvider->getClass(stdClass::class)), new ObjectWithoutClassType(), TrinaryLogic::createMaybe(), ], 29 => [ new ObjectWithoutClassType(), - new ThisType($broker->getClass(\stdClass::class)), + new ThisType($reflectionProvider->getClass(stdClass::class)), TrinaryLogic::createYes(), ], [ - new StaticType(Base::class), + new StaticType($reflectionProvider->getClass(Base::class)), new ObjectType(Child::class), TrinaryLogic::createMaybe(), ], [ - new StaticType(Base::class), - new StaticType(FinalChild::class), + new StaticType($reflectionProvider->getClass(Base::class)), + new StaticType($reflectionProvider->getClass(FinalChild::class)), TrinaryLogic::createYes(), ], [ - new StaticType(Base::class), - new StaticType(Child::class), + new StaticType($reflectionProvider->getClass(Base::class)), + new StaticType($reflectionProvider->getClass(Child::class)), TrinaryLogic::createYes(), ], [ - new StaticType(Base::class), + new StaticType($reflectionProvider->getClass(Base::class)), new ObjectType(FinalChild::class), TrinaryLogic::createYes(), ], @@ -254,9 +258,6 @@ public function dataIsSuperTypeOf(): array /** * @dataProvider dataIsSuperTypeOf - * @param Type $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(Type $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -264,33 +265,33 @@ public function testIsSuperTypeOf(Type $type, Type $otherType, TrinaryLogic $exp $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } public function dataEquals(): array { - $reflectionProvider = Broker::getInstance(); + $reflectionProvider = $this->createReflectionProvider(); return [ [ - new ThisType($reflectionProvider->getClass(\Exception::class)), - new ThisType($reflectionProvider->getClass(\Exception::class)), + new ThisType($reflectionProvider->getClass(Exception::class)), + new ThisType($reflectionProvider->getClass(Exception::class)), true, ], [ - new ThisType($reflectionProvider->getClass(\Exception::class)), - new ThisType($reflectionProvider->getClass(\InvalidArgumentException::class)), + new ThisType($reflectionProvider->getClass(Exception::class)), + new ThisType($reflectionProvider->getClass(InvalidArgumentException::class)), false, ], [ - new ThisType($reflectionProvider->getClass(\Exception::class)), - new StaticType(\Exception::class), + new ThisType($reflectionProvider->getClass(Exception::class)), + new StaticType($reflectionProvider->getClass(Exception::class)), false, ], [ - new ThisType($reflectionProvider->getClass(\Exception::class)), - new StaticType(\InvalidArgumentException::class), + new ThisType($reflectionProvider->getClass(Exception::class)), + new StaticType($reflectionProvider->getClass(InvalidArgumentException::class)), false, ], ]; @@ -298,9 +299,6 @@ public function dataEquals(): array /** * @dataProvider dataEquals - * @param StaticType $type - * @param StaticType $otherType - * @param bool $expected */ public function testEquals(StaticType $type, StaticType $otherType, bool $expected): void { diff --git a/tests/PHPStan/Type/StringTypeTest.php b/tests/PHPStan/Type/StringTypeTest.php index 2aabdb976a..d31be3e1fd 100644 --- a/tests/PHPStan/Type/StringTypeTest.php +++ b/tests/PHPStan/Type/StringTypeTest.php @@ -2,7 +2,8 @@ namespace PHPStan\Type; -use PHPStan\Testing\TestCase; +use Exception; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Constant\ConstantStringType; @@ -10,9 +11,11 @@ use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; +use stdClass; use Test\ClassWithToString; +use function sprintf; -class StringTypeTest extends TestCase +class StringTypeTest extends PHPStanTestCase { public function dataIsSuperTypeOf(): array @@ -20,7 +23,7 @@ public function dataIsSuperTypeOf(): array return [ [ new StringType(), - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), TrinaryLogic::createYes(), ], [ @@ -29,7 +32,7 @@ public function dataIsSuperTypeOf(): array TemplateTypeScope::createWithFunction('foo'), 'T', new StringType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), TrinaryLogic::createYes(), ], @@ -38,7 +41,7 @@ public function dataIsSuperTypeOf(): array TemplateTypeScope::createWithFunction('foo'), 'T', new StringType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new StringType(), TrinaryLogic::createMaybe(), @@ -49,7 +52,7 @@ public function dataIsSuperTypeOf(): array TemplateTypeScope::createWithFunction('foo'), 'T', new StringType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), TrinaryLogic::createMaybe(), ], @@ -58,18 +61,18 @@ public function dataIsSuperTypeOf(): array TemplateTypeScope::createWithFunction('foo'), 'T', new StringType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new ClassStringType(), TrinaryLogic::createMaybe(), ], [ - new GenericClassStringType(new ObjectType(\stdClass::class)), + new GenericClassStringType(new ObjectType(stdClass::class)), TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('foo'), 'T', new StringType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), TrinaryLogic::createMaybe(), ], @@ -78,9 +81,9 @@ public function dataIsSuperTypeOf(): array TemplateTypeScope::createWithFunction('foo'), 'T', new StringType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), - new GenericClassStringType(new ObjectType(\stdClass::class)), + new GenericClassStringType(new ObjectType(stdClass::class)), TrinaryLogic::createMaybe(), ], ]; @@ -95,7 +98,7 @@ public function testIsSuperTypeOf(StringType $type, Type $otherType, TrinaryLogi $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -122,7 +125,7 @@ public function dataAccepts(): iterable TemplateTypeScope::createWithFunction('foo'), 'T', new StringType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), TrinaryLogic::createYes(), ]; @@ -132,7 +135,7 @@ public function dataAccepts(): iterable TemplateTypeScope::createWithFunction('foo'), 'T', new StringType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new StringType(), TrinaryLogic::createYes(), @@ -143,10 +146,10 @@ public function dataAccepts(): iterable TemplateTypeScope::createWithFunction('foo'), 'T', new StringType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), )->toArgument(), new StringType(), - TrinaryLogic::createNo(), + TrinaryLogic::createMaybe(), ]; yield [ @@ -154,13 +157,13 @@ public function dataAccepts(): iterable TemplateTypeScope::createWithFunction('foo'), 'T', new StringType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), )->toArgument(), TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('foo'), 'T', new StringType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), )->toArgument(), TrinaryLogic::createYes(), ]; @@ -168,9 +171,6 @@ public function dataAccepts(): iterable /** * @dataProvider dataAccepts - * @param \PHPStan\Type\StringType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testAccepts(StringType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -178,7 +178,7 @@ public function testAccepts(StringType $type, Type $otherType, TrinaryLogic $exp $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } @@ -220,9 +220,6 @@ public function dataEquals(): array /** * @dataProvider dataEquals - * @param StringType $type - * @param Type $otherType - * @param bool $expectedResult */ public function testEquals(StringType $type, Type $otherType, bool $expectedResult): void { @@ -230,7 +227,7 @@ public function testEquals(StringType $type, Type $otherType, bool $expectedResu $this->assertSame( $expectedResult, $actualResult, - sprintf('%s->equals(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s->equals(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } diff --git a/tests/PHPStan/Type/TemplateTypeTest.php b/tests/PHPStan/Type/TemplateTypeTest.php index f52ad4b3b0..36281bdba1 100644 --- a/tests/PHPStan/Type/TemplateTypeTest.php +++ b/tests/PHPStan/Type/TemplateTypeTest.php @@ -2,59 +2,68 @@ namespace PHPStan\Type; +use DateTime; +use DateTimeInterface; +use Exception; +use Iterator; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; +use stdClass; +use Throwable; +use Traversable; +use function array_map; +use function assert; +use function sprintf; -class TemplateTypeTest extends \PHPStan\Testing\TestCase +class TemplateTypeTest extends PHPStanTestCase { public function dataAccepts(): array { - $templateType = static function (string $name, ?Type $bound, ?string $functionName = null): Type { - return TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction($functionName ?? '_'), - $name, - $bound, - TemplateTypeVariance::createInvariant() - ); - }; + $templateType = static fn (string $name, ?Type $bound, ?string $functionName = null): Type => TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction($functionName ?? '_'), + $name, + $bound, + TemplateTypeVariance::createInvariant(), + ); return [ - [ + 0 => [ $templateType('T', new ObjectType('DateTime')), new ObjectType('DateTime'), TrinaryLogic::createYes(), - TrinaryLogic::createNo(), + TrinaryLogic::createMaybe(), ], - [ + 1 => [ $templateType('T', new ObjectType('DateTimeInterface')), new ObjectType('DateTime'), TrinaryLogic::createYes(), - TrinaryLogic::createNo(), + TrinaryLogic::createMaybe(), ], - [ + 2 => [ $templateType('T', new ObjectType('DateTime')), $templateType('T', new ObjectType('DateTime')), TrinaryLogic::createYes(), TrinaryLogic::createYes(), ], - [ + 3 => [ $templateType('T', new ObjectType('DateTime'), 'a'), $templateType('T', new ObjectType('DateTime'), 'b'), TrinaryLogic::createMaybe(), - TrinaryLogic::createNo(), + TrinaryLogic::createMaybe(), ], - [ + 4 => [ $templateType('T', null), new MixedType(), TrinaryLogic::createYes(), TrinaryLogic::createYes(), ], - [ + 5 => [ $templateType('T', null), new IntersectionType([ new ObjectWithoutClassType(), @@ -63,6 +72,27 @@ public function dataAccepts(): array TrinaryLogic::createYes(), TrinaryLogic::createYes(), ], + 'accepts itself with a sub-type union bound' => [ + $templateType('T', new UnionType([ + new IntegerType(), + new StringType(), + ])), + $templateType('T', new IntegerType()), + TrinaryLogic::createYes(), + TrinaryLogic::createYes(), + ], + 'accepts itself with a sub-type object bound' => [ + $templateType('T', new ObjectWithoutClassType()), + $templateType('T', new ObjectType('stdClass')), + TrinaryLogic::createYes(), + TrinaryLogic::createYes(), + ], + 'does not accept ObjectType that is a super type of bound' => [ + $templateType('T', new ObjectType(Iterator::class)), + new ObjectType(Traversable::class), + TrinaryLogic::createNo(), + TrinaryLogic::createNo(), + ], ]; } @@ -73,7 +103,7 @@ public function testAccepts( Type $type, Type $otherType, TrinaryLogic $expectedAccept, - TrinaryLogic $expectedAcceptArg + TrinaryLogic $expectedAcceptArg, ): void { assert($type instanceof TemplateType); @@ -82,7 +112,7 @@ public function testAccepts( $this->assertSame( $expectedAccept->describe(), $actualResult->describe(), - sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); $type = $type->toArgument(); @@ -91,20 +121,18 @@ public function testAccepts( $this->assertSame( $expectedAcceptArg->describe(), $actualResult->describe(), - sprintf('%s -> accepts(%s) (Argument strategy)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s) (Argument strategy)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } public function dataIsSuperTypeOf(): array { - $templateType = static function (string $name, ?Type $bound, ?string $functionName = null): Type { - return TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction($functionName ?? '_'), - $name, - $bound, - TemplateTypeVariance::createInvariant() - ); - }; + $templateType = static fn (string $name, ?Type $bound, ?string $functionName = null): Type => TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction($functionName ?? '_'), + $name, + $bound, + TemplateTypeVariance::createInvariant(), + ); return [ 0 => [ @@ -141,7 +169,7 @@ public function dataIsSuperTypeOf(): array $templateType('T', new ObjectType('DateTime')), $templateType('T', new ObjectType('DateTimeInterface')), TrinaryLogic::createMaybe(), // (T of DateTime) isSuperTypeTo (T of DateTimeInterface) - TrinaryLogic::createMaybe(), // (T of DateTimeInterface) isSuperTypeTo (T of DateTime) + TrinaryLogic::createYes(), // (T of DateTimeInterface) isSuperTypeTo (T of DateTime) ], 6 => [ $templateType('T', new ObjectType('DateTime')), @@ -184,7 +212,7 @@ public function dataIsSuperTypeOf(): array ], 11 => [ $templateType('T', null), - new ObjectType(\DateTimeInterface::class), + new ObjectType(DateTimeInterface::class), TrinaryLogic::createMaybe(), // T isSuperTypeTo DateTimeInterface TrinaryLogic::createMaybe(), // DateTimeInterface isSuperTypeTo T ], @@ -196,59 +224,59 @@ public function dataIsSuperTypeOf(): array ], 13 => [ $templateType('T', new ObjectWithoutClassType()), - new ObjectType(\DateTimeInterface::class), + new ObjectType(DateTimeInterface::class), TrinaryLogic::createMaybe(), TrinaryLogic::createMaybe(), ], 14 => [ - $templateType('T', new ObjectType(\Throwable::class)), - new ObjectType(\Exception::class), + $templateType('T', new ObjectType(Throwable::class)), + new ObjectType(Exception::class), TrinaryLogic::createMaybe(), TrinaryLogic::createMaybe(), ], - [ + 15 => [ $templateType('T', new MixedType(true)), $templateType('U', new UnionType([new IntegerType(), new StringType()])), TrinaryLogic::createMaybe(), TrinaryLogic::createMaybe(), ], - [ + 16 => [ $templateType('T', new MixedType(true)), $templateType('U', new BenevolentUnionType([new IntegerType(), new StringType()])), TrinaryLogic::createMaybe(), TrinaryLogic::createMaybe(), ], - [ - $templateType('T', new ObjectType(\stdClass::class)), + 17 => [ + $templateType('T', new ObjectType(stdClass::class)), $templateType('U', new BenevolentUnionType([new IntegerType(), new StringType()])), TrinaryLogic::createNo(), TrinaryLogic::createNo(), ], - [ + 18 => [ $templateType('T', new BenevolentUnionType([new IntegerType(), new StringType()])), $templateType('T', new BenevolentUnionType([new IntegerType(), new StringType()])), TrinaryLogic::createYes(), TrinaryLogic::createYes(), ], - [ + 19 => [ $templateType('T', new UnionType([new IntegerType(), new StringType()])), $templateType('T', new UnionType([new IntegerType(), new StringType()])), TrinaryLogic::createYes(), TrinaryLogic::createYes(), ], - [ + 20 => [ $templateType('T', new UnionType([new IntegerType(), new StringType()])), $templateType('T', new BenevolentUnionType([new IntegerType(), new StringType()])), - TrinaryLogic::createMaybe(), - TrinaryLogic::createMaybe(), + TrinaryLogic::createYes(), + TrinaryLogic::createYes(), ], - [ + 21 => [ $templateType('T', new UnionType([new IntegerType(), new StringType()])), $templateType('T', new IntegerType()), - TrinaryLogic::createMaybe(), + TrinaryLogic::createYes(), TrinaryLogic::createMaybe(), ], - [ + 22 => [ $templateType('T', new BenevolentUnionType([new IntegerType(), new StringType()])), new UnionType([new BooleanType(), new FloatType(), new IntegerType(), new StringType(), new NullType()]), TrinaryLogic::createMaybe(), @@ -264,7 +292,7 @@ public function testIsSuperTypeOf( Type $type, Type $otherType, TrinaryLogic $expectedIsSuperType, - TrinaryLogic $expectedIsSuperTypeInverse + TrinaryLogic $expectedIsSuperTypeInverse, ): void { assert($type instanceof TemplateType); @@ -273,28 +301,26 @@ public function testIsSuperTypeOf( $this->assertSame( $expectedIsSuperType->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); $actualResult = $otherType->isSuperTypeOf($type); $this->assertSame( $expectedIsSuperTypeInverse->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())), ); } /** @return array}> */ public function dataInferTemplateTypes(): array { - $templateType = static function (string $name, ?Type $bound = null, ?string $functionName = null): Type { - return TemplateTypeFactory::create( - TemplateTypeScope::createWithFunction($functionName ?? '_'), - $name, - $bound, - TemplateTypeVariance::createInvariant() - ); - }; + $templateType = static fn (string $name, ?Type $bound = null, ?string $functionName = null): Type => TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction($functionName ?? '_'), + $name, + $bound, + TemplateTypeVariance::createInvariant(), + ); return [ 'simple' => [ @@ -303,33 +329,33 @@ public function dataInferTemplateTypes(): array ['T' => 'int'], ], 'object' => [ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), $templateType('T'), ['T' => 'DateTime'], ], 'object with bound' => [ - new ObjectType(\DateTime::class), - $templateType('T', new ObjectType(\DateTimeInterface::class)), + new ObjectType(DateTime::class), + $templateType('T', new ObjectType(DateTimeInterface::class)), ['T' => 'DateTime'], ], 'wrong object with bound' => [ - new ObjectType(\stdClass::class), - $templateType('T', new ObjectType(\DateTimeInterface::class)), + new ObjectType(stdClass::class), + $templateType('T', new ObjectType(DateTimeInterface::class)), [], ], 'template type' => [ - TemplateTypeHelper::toArgument($templateType('T', new ObjectType(\DateTimeInterface::class))), - $templateType('T', new ObjectType(\DateTimeInterface::class)), + TemplateTypeHelper::toArgument($templateType('T', new ObjectType(DateTimeInterface::class))), + $templateType('T', new ObjectType(DateTimeInterface::class)), ['T' => 'T of DateTimeInterface (function _(), argument)'], ], 'foreign template type' => [ - TemplateTypeHelper::toArgument($templateType('T', new ObjectType(\DateTimeInterface::class), 'a')), - $templateType('T', new ObjectType(\DateTimeInterface::class), 'b'), + TemplateTypeHelper::toArgument($templateType('T', new ObjectType(DateTimeInterface::class), 'a')), + $templateType('T', new ObjectType(DateTimeInterface::class), 'b'), ['T' => 'T of DateTimeInterface (function a(), argument)'], ], 'foreign template type, imcompatible bound' => [ - TemplateTypeHelper::toArgument($templateType('T', new ObjectType(\stdClass::class), 'a')), - $templateType('T', new ObjectType(\DateTime::class), 'b'), + TemplateTypeHelper::toArgument($templateType('T', new ObjectType(stdClass::class), 'a')), + $templateType('T', new ObjectType(DateTime::class), 'b'), [], ], ]; @@ -345,9 +371,7 @@ public function testResolveTemplateTypes(Type $received, Type $template, array $ $this->assertSame( $expectedTypes, - array_map(static function (Type $type): string { - return $type->describe(VerbosityLevel::precise()); - }, $result->getTypes()) + array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::precise()), $result->getTypes()), ); } diff --git a/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtension.php b/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtension.php index 1e82f949a5..41db098905 100644 --- a/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtension.php +++ b/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtension.php @@ -3,6 +3,7 @@ namespace PHPStan\Type; use PHPStan\Fixture\TestDecimal; +use function in_array; final class TestDecimalOperatorTypeSpecifyingExtension implements OperatorTypeSpecifyingExtension { diff --git a/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtensionTest.php b/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtensionTest.php index 546b79a7b2..3f64bd6221 100644 --- a/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtensionTest.php +++ b/tests/PHPStan/Type/TestDecimalOperatorTypeSpecifyingExtensionTest.php @@ -4,6 +4,7 @@ use PHPStan\Fixture\TestDecimal; use PHPUnit\Framework\TestCase; +use stdClass; class TestDecimalOperatorTypeSpecifyingExtensionTest extends TestCase { @@ -63,14 +64,14 @@ public function dataNotMatchingSidesProvider(): iterable { yield 'left' => [ '+', - new ObjectType(\stdClass::class), + new ObjectType(stdClass::class), new ObjectType(TestDecimal::class), ]; yield 'right' => [ '+', new ObjectType(TestDecimal::class), - new ObjectType(\stdClass::class), + new ObjectType(stdClass::class), ]; } diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index 2705f26ddf..297c5b32fa 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -2,7 +2,18 @@ namespace PHPStan\Type; -use PHPStan\Broker\Broker; +use CheckTypeFunctionCall\FinalClassWithMethodExists; +use CheckTypeFunctionCall\FinalClassWithPropertyExists; +use Closure; +use DateTime; +use DateTimeImmutable; +use DateTimeInterface; +use Exception; +use InvalidArgumentException; +use Iterator; +use PHPStan\Fixture\FinalClass; +use PHPStan\Testing\PHPStanTestCase; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Accessory\HasMethodType; use PHPStan\Type\Accessory\HasOffsetType; @@ -13,17 +24,31 @@ 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\TemplateBenevolentUnionType; +use PHPStan\Type\Generic\TemplateMixedType; use PHPStan\Type\Generic\TemplateObjectType; use PHPStan\Type\Generic\TemplateObjectWithoutClassType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; +use RecursionCallable\Foo; +use stdClass; +use Test\ClassWithNullableProperty; +use Test\ClassWithToString; +use Test\FirstInterface; +use Throwable; +use Traversable; +use function array_map; +use function array_reverse; +use function implode; +use function sprintf; +use const PHP_VERSION_ID; -class TypeCombinatorTest extends \PHPStan\Testing\TestCase +class TypeCombinatorTest extends PHPStanTestCase { public function dataAddNull(): array @@ -90,14 +115,12 @@ public function dataAddNull(): array /** * @dataProvider dataAddNull - * @param \PHPStan\Type\Type $type - * @param class-string<\PHPStan\Type\Type> $expectedTypeClass - * @param string $expectedTypeDescription + * @param class-string $expectedTypeClass */ public function testAddNull( Type $type, string $expectedTypeClass, - string $expectedTypeDescription + string $expectedTypeDescription, ): void { $result = TypeCombinator::addNull($type); @@ -107,14 +130,12 @@ public function testAddNull( /** * @dataProvider dataAddNull - * @param \PHPStan\Type\Type $type - * @param class-string<\PHPStan\Type\Type> $expectedTypeClass - * @param string $expectedTypeDescription + * @param class-string $expectedTypeClass */ public function testUnionWithNull( Type $type, string $expectedTypeClass, - string $expectedTypeDescription + string $expectedTypeDescription, ): void { $result = TypeCombinator::union($type, new NullType()); @@ -124,7 +145,8 @@ public function testUnionWithNull( public function dataRemoveNull(): array { - $reflectionProvider = Broker::getInstance(); + $reflectionProvider = $this->createReflectionProvider(); + return [ [ new MixedType(), @@ -184,7 +206,7 @@ public function dataRemoveNull(): array ], [ new UnionType([ - new ThisType($reflectionProvider->getClass(\Exception::class)), + new ThisType($reflectionProvider->getClass(Exception::class)), new NullType(), ]), ThisType::class, @@ -192,7 +214,7 @@ public function dataRemoveNull(): array ], [ new UnionType([ - new ThisType(\Exception::class), + new ThisType($reflectionProvider->getClass(Exception::class)), new NullType(), ]), ThisType::class, @@ -211,14 +233,12 @@ public function dataRemoveNull(): array /** * @dataProvider dataRemoveNull - * @param \PHPStan\Type\Type $type - * @param class-string<\PHPStan\Type\Type> $expectedTypeClass - * @param string $expectedTypeDescription + * @param class-string $expectedTypeClass */ public function testRemoveNull( Type $type, string $expectedTypeClass, - string $expectedTypeDescription + string $expectedTypeDescription, ): void { $result = TypeCombinator::removeNull($type); @@ -226,9 +246,9 @@ public function testRemoveNull( $this->assertInstanceOf($expectedTypeClass, $result); } - public function dataUnion(): array + public function dataUnion(): iterable { - return [ + yield from [ [ [ new StringType(), @@ -547,7 +567,7 @@ public function dataUnion(): array ], [ [ - new ObjectType(\RecursionCallable\Foo::class), + new ObjectType(Foo::class), new CallableType(), ], UnionType::class, @@ -659,7 +679,7 @@ public function dataUnion(): array ], [ [ - new ObjectType(\Closure::class), + new ObjectType(Closure::class), new ClosureType([], new MixedType(), false), ], ObjectType::class, @@ -680,7 +700,7 @@ public function dataUnion(): array new ConstantStringType('foo'), new ConstantStringType('bar'), ], [ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new IntegerType(), ]), new ConstantArrayType([ @@ -692,7 +712,7 @@ public function dataUnion(): array ]), ], ConstantArrayType::class, - 'array(\'foo\' => DateTimeImmutable|null, \'bar\' => int|string)', + 'array{foo: DateTimeImmutable|null, bar: int|string}', ], [ [ @@ -700,7 +720,7 @@ public function dataUnion(): array new ConstantStringType('foo'), new ConstantStringType('bar'), ], [ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new IntegerType(), ]), new ConstantArrayType([ @@ -710,7 +730,7 @@ public function dataUnion(): array ]), ], ConstantArrayType::class, - 'array(\'foo\' => DateTimeImmutable|null, ?\'bar\' => int)', + 'array{foo: DateTimeImmutable|null, bar?: int}', ], [ [ @@ -718,7 +738,7 @@ public function dataUnion(): array new ConstantStringType('foo'), new ConstantStringType('bar'), ], [ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new IntegerType(), ]), new ConstantArrayType([ @@ -732,19 +752,19 @@ public function dataUnion(): array ]), ], ConstantArrayType::class, - 'array(\'foo\' => DateTimeImmutable|null, \'bar\' => int|string, ?\'baz\' => int)', + 'array{foo: DateTimeImmutable|null, bar: int|string, baz?: int}', ], [ [ new ArrayType( new IntegerType(), - new ObjectType(\stdClass::class) + new ObjectType(stdClass::class), ), new ConstantArrayType([ new ConstantStringType('foo'), new ConstantStringType('bar'), ], [ - new ObjectType(\DateTimeImmutable::class), + new ObjectType(DateTimeImmutable::class), new IntegerType(), ]), ], @@ -840,8 +860,8 @@ public function dataUnion(): array new ConstantStringType('loremm'), new ConstantStringType('loremmm'), ], - StringType::class, - 'string', + UnionType::class, + "'bar'|'barr'|'baz'|'bazz'|'foo'|'fooo'|'lorem'|'loremm'|'loremmm'", ], [ [ @@ -882,7 +902,7 @@ public function dataUnion(): array [ new ObjectWithoutClassType(), new ConstantStringType('foo'), - ] + ], ), new CallableType(), ]), @@ -895,13 +915,13 @@ public function dataUnion(): array [ new ObjectWithoutClassType(), new ConstantStringType('foo'), - ] + ], ), new CallableType(), ]), ], IntersectionType::class, - 'array(object, \'foo\')&callable(): mixed', + 'array{object, \'foo\'}&callable(): mixed', ], [ [ @@ -1129,7 +1149,7 @@ public function dataUnion(): array TemplateTypeScope::createWithFunction('a'), 'T', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new ObjectType('DateTime'), ], @@ -1142,7 +1162,7 @@ public function dataUnion(): array TemplateTypeScope::createWithFunction('a'), 'T', new ObjectType('DateTime'), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new ObjectType('DateTime'), ], @@ -1155,13 +1175,13 @@ public function dataUnion(): array TemplateTypeScope::createWithFunction('a'), 'T', new ObjectType('DateTime'), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), 'T', new ObjectType('DateTime'), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), ], TemplateType::class, @@ -1173,18 +1193,124 @@ public function dataUnion(): array TemplateTypeScope::createWithFunction('a'), 'T', new ObjectType('DateTime'), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), 'U', new ObjectType('DateTime'), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), ], UnionType::class, 'T of DateTime (function a(), parameter)|U of DateTime (function a(), parameter)', ], + 'bug6210-1' => [ + [ + new ObjectWithoutClassType(), + new IntersectionType([ + new ObjectWithoutClassType(), + new HasMethodType('getId'), + ]), + ], + ObjectWithoutClassType::class, + 'object', + ], + 'bug6210-2' => [ + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + null, + TemplateTypeVariance::createInvariant(), + ), + new IntersectionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + null, + TemplateTypeVariance::createInvariant(), + ), + new HasMethodType('getId'), + ]), + ], + TemplateMixedType::class, + 'T (function a(), parameter)=explicit', + ], + 'bug6210-3' => [ + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + new ObjectWithoutClassType(), + TemplateTypeVariance::createInvariant(), + ), + new IntersectionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + new ObjectWithoutClassType(), + TemplateTypeVariance::createInvariant(), + ), + new HasMethodType('getId'), + ]), + ], + TemplateObjectWithoutClassType::class, + 'T of object (function a(), parameter)', + ], + 'bug6210-4' => [ + [ + new ObjectWithoutClassType(), + new IntersectionType([ + new ObjectWithoutClassType(), + new HasPropertyType('getId'), + ]), + ], + ObjectWithoutClassType::class, + 'object', + ], + 'bug6210-5' => [ + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + null, + TemplateTypeVariance::createInvariant(), + ), + new IntersectionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + null, + TemplateTypeVariance::createInvariant(), + ), + new HasPropertyType('getId'), + ]), + ], + TemplateMixedType::class, + 'T (function a(), parameter)=explicit', + ], + 'bug6210-6' => [ + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + new ObjectWithoutClassType(), + TemplateTypeVariance::createInvariant(), + ), + new IntersectionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('a'), + 'T', + new ObjectWithoutClassType(), + TemplateTypeVariance::createInvariant(), + ), + new HasPropertyType('getId'), + ]), + ], + TemplateObjectWithoutClassType::class, + 'T of object (function a(), parameter)', + ], [ [ new BenevolentUnionType([new IntegerType(), new StringType()]), @@ -1270,7 +1396,7 @@ public function dataUnion(): array [ [ new ClassStringType(), - new ConstantStringType(\stdClass::class), + new ConstantStringType(stdClass::class), ], ClassStringType::class, 'class-string', @@ -1293,15 +1419,15 @@ public function dataUnion(): array ], [ [ - new ConstantStringType(\Exception::class), - new GenericClassStringType(new ObjectType(\Exception::class)), + new ConstantStringType(Exception::class), + new GenericClassStringType(new ObjectType(Exception::class)), ], GenericClassStringType::class, 'class-string', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), new ClassStringType(), ], ClassStringType::class, @@ -1309,7 +1435,7 @@ public function dataUnion(): array ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), new StringType(), ], StringType::class, @@ -1317,64 +1443,64 @@ public function dataUnion(): array ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), ], GenericClassStringType::class, 'class-string', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\Throwable::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(Throwable::class)), ], GenericClassStringType::class, 'class-string', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\InvalidArgumentException::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(InvalidArgumentException::class)), ], GenericClassStringType::class, 'class-string', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\stdClass::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(stdClass::class)), ], UnionType::class, 'class-string|class-string', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new ConstantStringType(\Exception::class), + new GenericClassStringType(new ObjectType(Exception::class)), + new ConstantStringType(Exception::class), ], GenericClassStringType::class, 'class-string', ], [ [ - new GenericClassStringType(new ObjectType(\Throwable::class)), - new ConstantStringType(\Exception::class), + new GenericClassStringType(new ObjectType(Throwable::class)), + new ConstantStringType(Exception::class), ], GenericClassStringType::class, 'class-string', ], [ [ - new GenericClassStringType(new ObjectType(\InvalidArgumentException::class)), - new ConstantStringType(\Exception::class), + new GenericClassStringType(new ObjectType(InvalidArgumentException::class)), + new ConstantStringType(Exception::class), ], UnionType::class, '\'Exception\'|class-string', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new ConstantStringType(\stdClass::class), + new GenericClassStringType(new ObjectType(Exception::class)), + new ConstantStringType(stdClass::class), ], UnionType::class, '\'stdClass\'|class-string', @@ -1497,10 +1623,10 @@ public function dataUnion(): array [ [ new GenericObjectType(Variance\Invariant::class, [ - new ObjectType(\DateTimeInterface::class), + new ObjectType(DateTimeInterface::class), ]), new GenericObjectType(Variance\Invariant::class, [ - new ObjectType(\DateTimeInterface::class), + new ObjectType(DateTimeInterface::class), ]), ], GenericObjectType::class, @@ -1509,10 +1635,10 @@ public function dataUnion(): array [ [ new GenericObjectType(Variance\Invariant::class, [ - new ObjectType(\DateTimeInterface::class), + new ObjectType(DateTimeInterface::class), ]), new GenericObjectType(Variance\Invariant::class, [ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), ]), ], UnionType::class, @@ -1521,10 +1647,10 @@ public function dataUnion(): array [ [ new GenericObjectType(Variance\Covariant::class, [ - new ObjectType(\DateTimeInterface::class), + new ObjectType(DateTimeInterface::class), ]), new GenericObjectType(Variance\Covariant::class, [ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), ]), ], GenericObjectType::class, @@ -1536,7 +1662,7 @@ public function dataUnion(): array TemplateTypeScope::createWithFunction('a'), 'T', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new ObjectWithoutClassType(), ], @@ -1549,9 +1675,9 @@ public function dataUnion(): array TemplateTypeScope::createWithFunction('a'), 'T', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), - new ObjectType(\stdClass::class), + new ObjectType(stdClass::class), ], UnionType::class, 'stdClass|T of object (function a(), parameter)', @@ -1562,7 +1688,7 @@ public function dataUnion(): array TemplateTypeScope::createWithFunction('a'), 'T', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new MixedType(), ], @@ -1575,13 +1701,13 @@ public function dataUnion(): array TemplateTypeScope::createWithFunction('a'), 'T', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), 'K', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), ], UnionType::class, @@ -1593,13 +1719,13 @@ public function dataUnion(): array TemplateTypeScope::createWithFunction('a'), 'T', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), 'K', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), ], UnionType::class, @@ -1610,14 +1736,14 @@ public function dataUnion(): array TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), 'T', - new ObjectType(\Exception::class), - TemplateTypeVariance::createInvariant() + new ObjectType(Exception::class), + TemplateTypeVariance::createInvariant(), ), TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), 'K', - new ObjectType(\stdClass::class), - TemplateTypeVariance::createInvariant() + new ObjectType(stdClass::class), + TemplateTypeVariance::createInvariant(), ), ], UnionType::class, @@ -1625,11 +1751,11 @@ public function dataUnion(): array ], [ [ - new ObjectType(\DateTimeImmutable::class), - new ObjectType(\DateTimeInterface::class, new ObjectType(\DateTimeImmutable::class)), + new ObjectType(DateTimeImmutable::class), + new ObjectType(DateTimeInterface::class, new ObjectType(DateTimeImmutable::class)), ], ObjectType::class, - \DateTimeInterface::class, + DateTimeInterface::class, ], [ [ @@ -1649,7 +1775,7 @@ public function dataUnion(): array ]), ], UnionType::class, - 'array()|array(string)', + 'array{}|array{string}', ], [ [ @@ -1661,7 +1787,7 @@ public function dataUnion(): array ], 1, [0]), ], UnionType::class, - 'array()|array(?0 => string)', + 'array{}|array{0?: string}', ], [ [ @@ -1681,7 +1807,7 @@ public function dataUnion(): array ]), ], UnionType::class, - 'array(\'a\' => int, \'b\' => int)|array(\'c\' => int, \'d\' => int)', + 'array{a: int, b: int}|array{c: int, d: int}', ], [ [ @@ -1699,7 +1825,7 @@ public function dataUnion(): array ]), ], ConstantArrayType::class, - 'array(\'a\' => int, ?\'b\' => int)', + 'array{a: int, b?: int}', ], [ [ @@ -1719,7 +1845,7 @@ public function dataUnion(): array ]), ], UnionType::class, - 'array(\'a\' => int, \'b\' => int)|array(\'b\' => int, \'c\' => int)', + 'array{a: int, b: int}|array{b: int, c: int}', ], [ [ @@ -1743,7 +1869,7 @@ public function dataUnion(): array StaticTypeFactory::falsey(), ], UnionType::class, - '0|0.0|\'\'|\'0\'|array()|false|null', + '0|0.0|\'\'|\'0\'|array{}|false|null', ], [ [ @@ -1751,7 +1877,7 @@ public function dataUnion(): array StaticTypeFactory::truthy(), ], MixedType::class, - 'mixed~0|0.0|\'\'|\'0\'|array()|false|null=implicit', + 'mixed~0|0.0|\'\'|\'0\'|array{}|false|null=implicit', ], [ [ @@ -1768,19 +1894,205 @@ public function dataUnion(): array TemplateBenevolentUnionType::class, 'T of (int|string) (function foo(), parameter)', ], + [ + [ + new ConstantStringType(''), + new IntersectionType([ + new StringType(), + new AccessoryNonEmptyStringType(), + ]), + ], + StringType::class, + 'string', + ], + [ + [ + new StringType(), + new UnionType([ + new ConstantStringType(''), + new ConstantStringType('0'), + new ConstantBooleanType(false), + ]), + ], + UnionType::class, + 'string|false', + ], + [ + [ + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new UnionType([ + new ConstantStringType(''), + new ConstantStringType('0'), + new ConstantBooleanType(false), + ]), + ], + UnionType::class, + 'string|false', + ], + [ + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithFunction('doFoo'), + 'T', + new UnionType([ + new StringType(), + new IntegerType(), + new FloatType(), + new BooleanType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ], + UnionType::class, + '(T of bool|float|int|string (function doFoo(), parameter))|null', + ], + [ + [ + new UnionType([ + new ArrayType(new MixedType(), new MixedType()), + IntegerRangeType::fromInterval(null, -1), + IntegerRangeType::fromInterval(1, null), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'TCode', + new UnionType([new ArrayType(new IntegerType(), new IntegerType()), new IntegerType()]), + TemplateTypeVariance::createInvariant(), + ), + ], + UnionType::class, + 'array|int|int<1, max>|(TCode of array|int (class Foo, parameter))', + ], + [ + [ + new UnionType([ + new ArrayType(new MixedType(), new MixedType()), + new CallableType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'TCode', + new UnionType([new ArrayType(new IntegerType(), new IntegerType()), new IntegerType()]), + TemplateTypeVariance::createInvariant(), + ), + ], + UnionType::class, + 'array|(callable(): mixed)|(TCode of array|int (class Foo, parameter))', + ], + [ + [ + new MixedType(), + new StrictMixedType(), + ], + MixedType::class, + 'mixed=implicit', + ], + ]; + + if (PHP_VERSION_ID < 80100) { + return; + } + + yield [ + [ + new ObjectType('PHPStan\Fixture\TestEnum'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + ], + ObjectType::class, + 'PHPStan\Fixture\TestEnum', + ]; + yield [ + [ + new ObjectType('PHPStan\Fixture\TestEnum'), + new EnumCaseObjectType('PHPStan\Fixture\AnotherTestEnum', 'ONE'), + ], + UnionType::class, + 'PHPStan\Fixture\AnotherTestEnum::ONE|PHPStan\Fixture\TestEnum', + ]; + yield [ + [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + ], + EnumCaseObjectType::class, + 'PHPStan\Fixture\TestEnum::ONE', + ]; + yield [ + [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), + ], + UnionType::class, + 'PHPStan\Fixture\TestEnum::ONE|PHPStan\Fixture\TestEnum::TWO', + ]; + yield [ + [ + new ObjectType('PHPStan\Fixture\TestEnum'), + new ObjectType('PHPStan\Fixture\TestEnumInterface'), + ], + ObjectType::class, + 'PHPStan\Fixture\TestEnumInterface', + ]; + yield [ + [ + new ObjectType('PHPStan\Fixture\TestEnumInterface'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + ], + ObjectType::class, + 'PHPStan\Fixture\TestEnumInterface', + ]; + yield [ + [ + new ObjectType('PHPStan\Fixture\TestEnumInterface'), + new EnumCaseObjectType('PHPStan\Fixture\AnotherTestEnum', 'ONE'), + ], + UnionType::class, + 'PHPStan\Fixture\AnotherTestEnum::ONE|PHPStan\Fixture\TestEnumInterface', + ]; + yield [ + [ + new ObjectWithoutClassType(), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + ], + ObjectWithoutClassType::class, + 'object', + ]; + yield [ + [ + new ObjectType('stdClass'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + ], + UnionType::class, + 'PHPStan\Fixture\TestEnum::ONE|stdClass', + ]; + yield [ + [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\AnotherTestEnum', 'ONE'), + ], + UnionType::class, + 'PHPStan\Fixture\AnotherTestEnum::ONE|PHPStan\Fixture\TestEnum::ONE', + ]; + yield [ + [ + new MixedType(false, new IntegerRangeType(17, null)), + new IntegerRangeType(19, null), + ], + MixedType::class, + 'mixed~int<17, 18>=implicit', ]; } /** * @dataProvider dataUnion - * @param \PHPStan\Type\Type[] $types - * @param class-string<\PHPStan\Type\Type> $expectedTypeClass - * @param string $expectedTypeDescription + * @param Type[] $types + * @param class-string $expectedTypeClass */ public function testUnion( array $types, string $expectedTypeClass, - string $expectedTypeDescription + string $expectedTypeDescription, ): void { $actualType = TypeCombinator::union(...$types); @@ -1797,11 +2109,9 @@ public function testUnion( $expectedTypeDescription, $actualTypeDescription, sprintf('union(%s)', implode(', ', array_map( - static function (Type $type): string { - return $type->describe(VerbosityLevel::precise()); - }, - $types - ))) + static fn (Type $type): string => $type->describe(VerbosityLevel::precise()), + $types, + ))), ); $this->assertInstanceOf($expectedTypeClass, $actualType); @@ -1822,14 +2132,13 @@ static function (Type $type): string { /** * @dataProvider dataUnion - * @param \PHPStan\Type\Type[] $types - * @param class-string<\PHPStan\Type\Type> $expectedTypeClass - * @param string $expectedTypeDescription + * @param Type[] $types + * @param class-string $expectedTypeClass */ public function testUnionInversed( array $types, string $expectedTypeClass, - string $expectedTypeDescription + string $expectedTypeDescription, ): void { $types = array_reverse($types); @@ -1846,18 +2155,18 @@ public function testUnionInversed( $expectedTypeDescription, $actualTypeDescription, sprintf('union(%s)', implode(', ', array_map( - static function (Type $type): string { - return $type->describe(VerbosityLevel::precise()); - }, - $types - ))) + static fn (Type $type): string => $type->describe(VerbosityLevel::precise()), + $types, + ))), ); $this->assertInstanceOf($expectedTypeClass, $actualType); } - public function dataIntersect(): array + public function dataIntersect(): iterable { - return [ + $reflectionProvider = $this->createReflectionProvider(); + + yield from [ [ [ new IterableType(new MixedType(), new StringType()), @@ -1891,8 +2200,8 @@ public function dataIntersect(): array TemplateTypeScope::createWithFunction('_'), 'T', null, - TemplateTypeVariance::createInvariant() - ) + TemplateTypeVariance::createInvariant(), + ), ), ], IntersectionType::class, @@ -1901,7 +2210,7 @@ public function dataIntersect(): array [ [ new ObjectType('Foo'), - new StaticType('Foo'), + new StaticType($reflectionProvider->getClass('Foo')), ], StaticType::class, 'static(Foo)', @@ -2119,7 +2428,7 @@ public function dataIntersect(): array ], [ [ - new ObjectType(\Test\ClassWithToString::class), + new ObjectType(ClassWithToString::class), new HasMethodType('__toString'), ], ObjectType::class, @@ -2127,7 +2436,7 @@ public function dataIntersect(): array ], [ [ - new ObjectType(\CheckTypeFunctionCall\FinalClassWithMethodExists::class), + new ObjectType(FinalClassWithMethodExists::class), new HasMethodType('doBar'), ], NeverType::class, @@ -2175,7 +2484,7 @@ public function dataIntersect(): array [ new UnionType([ new ObjectType(\Test\Foo::class), - new ObjectType(\Test\FirstInterface::class), + new ObjectType(FirstInterface::class), ]), new HasMethodType('__toString'), ], @@ -2192,7 +2501,7 @@ public function dataIntersect(): array ], [ [ - new ObjectType(\Test\ClassWithNullableProperty::class), + new ObjectType(ClassWithNullableProperty::class), new HasPropertyType('foo'), ], ObjectType::class, @@ -2200,7 +2509,7 @@ public function dataIntersect(): array ], [ [ - new ObjectType(\CheckTypeFunctionCall\FinalClassWithPropertyExists::class), + new ObjectType(FinalClassWithPropertyExists::class), new HasPropertyType('barProperty'), ], NeverType::class, @@ -2248,7 +2557,7 @@ public function dataIntersect(): array [ new UnionType([ new ObjectType(\Test\Foo::class), - new ObjectType(\Test\FirstInterface::class), + new ObjectType(FirstInterface::class), ]), new HasPropertyType('fooProperty'), ], @@ -2294,18 +2603,18 @@ public function dataIntersect(): array [ new ConstantArrayType( [new ConstantStringType('a')], - [new ConstantStringType('foo')] + [new ConstantStringType('foo')], ), new HasOffsetType(new ConstantStringType('a')), ], ConstantArrayType::class, - 'array(\'a\' => \'foo\')', + 'array{a: \'foo\'}', ], [ [ new ConstantArrayType( [new ConstantStringType('a')], - [new ConstantStringType('foo')] + [new ConstantStringType('foo')], ), new HasOffsetType(new ConstantStringType('b')), ], @@ -2325,36 +2634,36 @@ public function dataIntersect(): array TypeCombinator::union( new ConstantArrayType( [new ConstantStringType('a')], - [new ConstantStringType('foo')] + [new ConstantStringType('foo')], ), new ConstantArrayType( [new ConstantStringType('b')], - [new ConstantStringType('foo')] - ) + [new ConstantStringType('foo')], + ), ), new HasOffsetType(new ConstantStringType('b')), ], ConstantArrayType::class, - 'array(\'b\' => \'foo\')', + 'array{b: \'foo\'}', ], [ [ TypeCombinator::union( new ConstantArrayType( [new ConstantStringType('a')], - [new ConstantStringType('foo')] + [new ConstantStringType('foo')], ), - new ClosureType([], new MixedType(), false) + new ClosureType([], new MixedType(), false), ), new HasOffsetType(new ConstantStringType('a')), ], ConstantArrayType::class, - 'array(\'a\' => \'foo\')', + 'array{a: \'foo\'}', ], [ [ new ClosureType([], new MixedType(), false), - new ObjectType(\Closure::class), + new ObjectType(Closure::class), ], ClosureType::class, 'Closure(): mixed', @@ -2392,7 +2701,7 @@ public function dataIntersect(): array new NonEmptyArrayType(), ], IntersectionType::class, - 'array&nonEmpty', + 'non-empty-array', ], [ [ @@ -2411,7 +2720,7 @@ public function dataIntersect(): array new NonEmptyArrayType(), ], IntersectionType::class, - 'array&nonEmpty', + 'non-empty-array', ], [ [ @@ -2421,12 +2730,12 @@ public function dataIntersect(): array new ConstantIntegerType(0), ], [ new StringType(), - ]) + ]), ), new NonEmptyArrayType(), ], ConstantArrayType::class, - 'array(string)', + 'array{string}', ], [ [ @@ -2496,7 +2805,7 @@ public function dataIntersect(): array TemplateTypeScope::createWithFunction('a'), 'T', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new ObjectType('DateTime'), ], @@ -2509,7 +2818,7 @@ public function dataIntersect(): array TemplateTypeScope::createWithFunction('a'), 'T', new ObjectType('DateTime'), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new ObjectType('DateTime'), ], @@ -2522,13 +2831,13 @@ public function dataIntersect(): array TemplateTypeScope::createWithFunction('a'), 'T', new ObjectType('DateTime'), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), 'T', new ObjectType('DateTime'), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), ], TemplateType::class, @@ -2540,13 +2849,13 @@ public function dataIntersect(): array TemplateTypeScope::createWithFunction('a'), 'T', new ObjectType('DateTime'), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), TemplateTypeFactory::create( TemplateTypeScope::createWithFunction('a'), 'U', new ObjectType('DateTime'), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), ], IntersectionType::class, @@ -2558,7 +2867,7 @@ public function dataIntersect(): array TemplateTypeScope::createWithFunction('a'), 'T', null, - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new MixedType(), ], @@ -2576,7 +2885,7 @@ public function dataIntersect(): array [ [ new ClassStringType(), - new ConstantStringType(\stdClass::class), + new ConstantStringType(stdClass::class), ], ConstantStringType::class, '\'stdClass\'', @@ -2599,15 +2908,15 @@ public function dataIntersect(): array ], [ [ - new ConstantStringType(\Exception::class), - new GenericClassStringType(new ObjectType(\Exception::class)), + new ConstantStringType(Exception::class), + new GenericClassStringType(new ObjectType(Exception::class)), ], ConstantStringType::class, '\'Exception\'', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), new ClassStringType(), ], GenericClassStringType::class, @@ -2615,7 +2924,7 @@ public function dataIntersect(): array ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), new StringType(), ], GenericClassStringType::class, @@ -2623,64 +2932,64 @@ public function dataIntersect(): array ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(Exception::class)), ], GenericClassStringType::class, 'class-string', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\Throwable::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(Throwable::class)), ], GenericClassStringType::class, 'class-string', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\InvalidArgumentException::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(InvalidArgumentException::class)), ], GenericClassStringType::class, 'class-string', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new GenericClassStringType(new ObjectType(\stdClass::class)), + new GenericClassStringType(new ObjectType(Exception::class)), + new GenericClassStringType(new ObjectType(stdClass::class)), ], NeverType::class, '*NEVER*', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new ConstantStringType(\Exception::class), + new GenericClassStringType(new ObjectType(Exception::class)), + new ConstantStringType(Exception::class), ], ConstantStringType::class, '\'Exception\'', ], [ [ - new GenericClassStringType(new ObjectType(\Throwable::class)), - new ConstantStringType(\Exception::class), + new GenericClassStringType(new ObjectType(Throwable::class)), + new ConstantStringType(Exception::class), ], ConstantStringType::class, '\'Exception\'', ], [ [ - new GenericClassStringType(new ObjectType(\InvalidArgumentException::class)), - new ConstantStringType(\Exception::class), + new GenericClassStringType(new ObjectType(InvalidArgumentException::class)), + new ConstantStringType(Exception::class), ], NeverType::class, '*NEVER*', ], [ [ - new GenericClassStringType(new ObjectType(\Exception::class)), - new ConstantStringType(\stdClass::class), + new GenericClassStringType(new ObjectType(Exception::class)), + new ConstantStringType(stdClass::class), ], NeverType::class, '*NEVER*', @@ -2735,7 +3044,7 @@ public function dataIntersect(): array ], [ [ - new ObjectType(\Traversable::class), + new ObjectType(Traversable::class), new IterableType(new MixedType(), new MixedType()), ], ObjectType::class, @@ -2743,7 +3052,7 @@ public function dataIntersect(): array ], [ [ - new ObjectType(\Traversable::class), + new ObjectType(Traversable::class), new IterableType(new MixedType(), new MixedType()), ], ObjectType::class, @@ -2751,7 +3060,7 @@ public function dataIntersect(): array ], [ [ - new ObjectType(\Traversable::class), + new ObjectType(Traversable::class), new IterableType(new MixedType(), new MixedType(true)), ], IntersectionType::class, @@ -2759,7 +3068,7 @@ public function dataIntersect(): array ], [ [ - new ObjectType(\Traversable::class), + new ObjectType(Traversable::class), new IterableType(new MixedType(true), new MixedType()), ], IntersectionType::class, @@ -2767,7 +3076,7 @@ public function dataIntersect(): array ], [ [ - new ObjectType(\Traversable::class), + new ObjectType(Traversable::class), new IterableType(new MixedType(true), new MixedType(true)), ], IntersectionType::class, @@ -2800,10 +3109,10 @@ public function dataIntersect(): array [ [ new GenericObjectType(Variance\Covariant::class, [ - new ObjectType(\DateTimeInterface::class), + new ObjectType(DateTimeInterface::class), ]), new GenericObjectType(Variance\Covariant::class, [ - new ObjectType(\DateTime::class), + new ObjectType(DateTime::class), ]), ], GenericObjectType::class, @@ -2815,7 +3124,7 @@ public function dataIntersect(): array TemplateTypeScope::createWithFunction('a'), 'T', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new ObjectWithoutClassType(), ], @@ -2828,9 +3137,9 @@ public function dataIntersect(): array TemplateTypeScope::createWithFunction('a'), 'T', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), - new ObjectType(\stdClass::class), + new ObjectType(stdClass::class), ], IntersectionType::class, 'stdClass&T of object (function a(), parameter)', @@ -2841,7 +3150,7 @@ public function dataIntersect(): array TemplateTypeScope::createWithFunction('a'), 'T', new ObjectWithoutClassType(), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new MixedType(), ], @@ -2858,7 +3167,7 @@ public function dataIntersect(): array ], [ [ - new ConstantStringType(\stdClass::class), + new ConstantStringType(stdClass::class), new ClassStringType(), ], ConstantStringType::class, @@ -2866,16 +3175,16 @@ public function dataIntersect(): array ], [ [ - new ObjectType(\DateTimeInterface::class), - new ObjectType(\Iterator::class), + new ObjectType(DateTimeInterface::class), + new ObjectType(Iterator::class), ], IntersectionType::class, 'DateTimeInterface&Iterator', ], [ [ - new ObjectType(\DateTimeInterface::class), - new GenericObjectType(\Iterator::class, [new MixedType(), new MixedType()]), + new ObjectType(DateTimeInterface::class), + new GenericObjectType(Iterator::class, [new MixedType(), new MixedType()]), ], IntersectionType::class, 'DateTimeInterface&Iterator', @@ -2892,7 +3201,7 @@ public function dataIntersect(): array new HasOffsetType(new ConstantStringType('a')), ], ConstantArrayType::class, - 'array(\'a\' => int, \'b\' => int)', + 'array{a: int, b: int}', ], [ [ @@ -2932,7 +3241,7 @@ public function dataIntersect(): array new AccessoryNumericStringType(), ], IntersectionType::class, - 'string&numeric', + 'numeric-string', ], [ [ @@ -2959,7 +3268,7 @@ public function dataIntersect(): array TemplateTypeScope::createWithFunction('my_array_keys'), 'T', new BenevolentUnionType([new IntegerType(), new StringType()]), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new UnionType([new IntegerType(), new StringType()]), ], @@ -2972,7 +3281,7 @@ public function dataIntersect(): array TemplateTypeScope::createWithFunction('my_array_keys'), 'T', new BenevolentUnionType([new IntegerType(), new StringType()]), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new BenevolentUnionType([new IntegerType(), new StringType()]), ], @@ -2985,26 +3294,134 @@ public function dataIntersect(): array TemplateTypeScope::createWithFunction('my_array_keys'), 'T', new UnionType([new IntegerType(), new StringType()]), - TemplateTypeVariance::createInvariant() + TemplateTypeVariance::createInvariant(), ), new UnionType([new IntegerType(), new StringType()]), ], UnionType::class, 'T of int|string (function my_array_keys(), parameter)', ], + [ + [ + new MixedType(), + new StrictMixedType(), + ], + StrictMixedType::class, + 'mixed', + ], + ]; + + if (PHP_VERSION_ID < 80100) { + return; + } + + yield [ + [ + new ObjectType('PHPStan\Fixture\TestEnum'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + ], + EnumCaseObjectType::class, + 'PHPStan\Fixture\TestEnum::ONE', + ]; + yield [ + [ + new ObjectType('PHPStan\Fixture\TestEnum'), + new EnumCaseObjectType(stdClass::class, 'ONE'), + ], + NeverType::class, + '*NEVER*', + ]; + yield [ + [ + new ObjectType('PHPStan\Fixture\TestEnumInterface'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + ], + EnumCaseObjectType::class, + 'PHPStan\Fixture\TestEnum::ONE', + ]; + yield [ + [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + ], + EnumCaseObjectType::class, + 'PHPStan\Fixture\TestEnum::ONE', + ]; + yield [ + [ + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'TWO'), + ], + NeverType::class, + '*NEVER*', + ]; + yield [ + [ + new ObjectType('PHPStan\Fixture\TestEnum'), + new ObjectType('PHPStan\Fixture\TestEnumInterface'), + ], + ObjectType::class, + 'PHPStan\Fixture\TestEnum', + ]; + yield [ + [ + new ObjectType('PHPStan\Fixture\TestEnumInterface'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + ], + EnumCaseObjectType::class, + 'PHPStan\Fixture\TestEnum::ONE', + ]; + yield [ + [ + new ObjectType('PHPStan\Fixture\TestEnumInterface'), + new EnumCaseObjectType('PHPStan\Fixture\AnotherTestEnum', 'ONE'), + ], + NeverType::class, + '*NEVER*', + ]; + yield [ + [ + new ObjectType(FinalClass::class), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + ], + NeverType::class, + '*NEVER*', + ]; + yield [ + [ + new ObjectWithoutClassType(), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + ], + EnumCaseObjectType::class, + 'PHPStan\Fixture\TestEnum::ONE', + ]; + yield [ + [ + new ObjectType('stdClass'), + new EnumCaseObjectType('PHPStan\Fixture\TestEnum', 'ONE'), + ], + NeverType::class, + '*NEVER*', + ]; + yield [ + [ + new MixedType(false, new IntegerRangeType(17, null)), + new MixedType(), + ], + MixedType::class, + 'mixed~int<17, max>=implicit', ]; } /** * @dataProvider dataIntersect - * @param \PHPStan\Type\Type[] $types - * @param class-string<\PHPStan\Type\Type> $expectedTypeClass - * @param string $expectedTypeDescription + * @param Type[] $types + * @param class-string $expectedTypeClass */ public function testIntersect( array $types, string $expectedTypeClass, - string $expectedTypeDescription + string $expectedTypeDescription, ): void { $actualType = TypeCombinator::intersect(...$types); @@ -3022,14 +3439,13 @@ public function testIntersect( /** * @dataProvider dataIntersect - * @param \PHPStan\Type\Type[] $types - * @param class-string<\PHPStan\Type\Type> $expectedTypeClass - * @param string $expectedTypeDescription + * @param Type[] $types + * @param class-string $expectedTypeClass */ public function testIntersectInversed( array $types, string $expectedTypeClass, - string $expectedTypeDescription + string $expectedTypeDescription, ): void { $actualType = TypeCombinator::intersect(...array_reverse($types)); @@ -3150,13 +3566,13 @@ public function dataRemove(): array StaticTypeFactory::truthy(), StaticTypeFactory::falsey(), MixedType::class, - 'mixed~0|0.0|\'\'|\'0\'|array()|false|null', + 'mixed~0|0.0|\'\'|\'0\'|array{}|false|null', ], [ StaticTypeFactory::falsey(), StaticTypeFactory::truthy(), UnionType::class, - '0|0.0|\'\'|\'0\'|array()|false|null', + '0|0.0|\'\'|\'0\'|array{}|false|null', ], [ new BooleanType(), @@ -3227,13 +3643,13 @@ public function dataRemove(): array ], [ new IterableType(new MixedType(), new MixedType()), - new ObjectType(\Traversable::class), + new ObjectType(Traversable::class), ArrayType::class, 'array', ], [ new IterableType(new MixedType(), new MixedType()), - new ObjectType(\Iterator::class), + new ObjectType(Iterator::class), IterableType::class, 'iterable', ], @@ -3253,13 +3669,13 @@ public function dataRemove(): array new BenevolentUnionType([new IntegerType(), new StringType()]), new ConstantStringType('foo'), UnionType::class, - 'int|string', + '(int|string)', ], [ new BenevolentUnionType([new IntegerType(), new StringType()]), new ConstantIntegerType(1), UnionType::class, - 'int|int<2, max>|string', + '(int|int<2, max>|string)', ], [ new BenevolentUnionType([new IntegerType(), new StringType()]), @@ -3271,7 +3687,7 @@ public function dataRemove(): array new ArrayType(new MixedType(), new MixedType()), new ConstantArrayType([], []), IntersectionType::class, - 'array&nonEmpty', + 'non-empty-array', ], [ TypeCombinator::union( @@ -3280,11 +3696,11 @@ public function dataRemove(): array new ConstantIntegerType(0), ], [ new StringType(), - ]) + ]), ), new ConstantArrayType([], []), ConstantArrayType::class, - 'array(string)', + 'array{string}', ], [ new IntersectionType([ @@ -3299,7 +3715,7 @@ public function dataRemove(): array new ArrayType(new MixedType(), new MixedType()), new NonEmptyArrayType(), ConstantArrayType::class, - 'array()', + 'array{}', ], [ new ArrayType(new MixedType(), new MixedType()), @@ -3473,7 +3889,7 @@ public function dataRemove(): array ], 2, [1]), new HasOffsetType(new ConstantIntegerType(1)), ConstantArrayType::class, - 'array(string)', + 'array{string}', ], [ new ConstantArrayType([ @@ -3500,26 +3916,34 @@ public function dataRemove(): array 'object', ], [ - new ObjectType(\stdClass::class), + new ObjectType(stdClass::class), new NeverType(), ObjectType::class, 'stdClass', ], + [ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new BooleanType(), + TemplateTypeVariance::createInvariant(), + ), + new ConstantBooleanType(false), + TemplateMixedType::class, // should be TemplateConstantBooleanType + 'T (class Foo, parameter)', // should be T of true + ], ]; } /** * @dataProvider dataRemove - * @param \PHPStan\Type\Type $fromType - * @param \PHPStan\Type\Type $type - * @param class-string<\PHPStan\Type\Type> $expectedTypeClass - * @param string $expectedTypeDescription + * @param class-string $expectedTypeClass */ public function testRemove( Type $fromType, Type $type, string $expectedTypeClass, - string $expectedTypeDescription + string $expectedTypeDescription, ): void { $result = TypeCombinator::remove($fromType, $type); @@ -3543,7 +3967,7 @@ public function testSpecificUnionConstantArray(): void } $resultType = TypeCombinator::union(...$arrays); $this->assertInstanceOf(ConstantArrayType::class, $resultType); - $this->assertSame('array(0 => string, ?\'test\' => string, ?1 => string, ?2 => string, ?3 => string, ?4 => string)', $resultType->describe(VerbosityLevel::precise())); + $this->assertSame('array{0: string, test?: string, 1?: string, 2?: string, 3?: string, 4?: string}', $resultType->describe(VerbosityLevel::precise())); } } diff --git a/tests/PHPStan/Type/UnionTypeTest.php b/tests/PHPStan/Type/UnionTypeTest.php index 1d8894733e..36636f4c05 100644 --- a/tests/PHPStan/Type/UnionTypeTest.php +++ b/tests/PHPStan/Type/UnionTypeTest.php @@ -2,8 +2,13 @@ namespace PHPStan\Type; +use DateTime; +use DateTimeImmutable; +use Exception; +use Iterator; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\PassedByReference; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Accessory\HasMethodType; @@ -20,8 +25,15 @@ use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; - -class UnionTypeTest extends \PHPStan\Testing\TestCase +use stdClass; +use function array_map; +use function array_merge; +use function array_reverse; +use function get_class; +use function implode; +use function sprintf; + +class UnionTypeTest extends PHPStanTestCase { public function dataIsCallable(): array @@ -31,9 +43,9 @@ public function dataIsCallable(): array TypeCombinator::union( new ConstantArrayType( [new ConstantIntegerType(0), new ConstantIntegerType(1)], - [new ConstantStringType('Closure'), new ConstantStringType('bind')] + [new ConstantStringType('Closure'), new ConstantStringType('bind')], ), - new ConstantStringType('array_push') + new ConstantStringType('array_push'), ), TrinaryLogic::createYes(), ], @@ -63,8 +75,6 @@ public function dataIsCallable(): array /** * @dataProvider dataIsCallable - * @param UnionType $type - * @param TrinaryLogic $expectedResult */ public function testIsCallable(UnionType $type, TrinaryLogic $expectedResult): void { @@ -72,13 +82,13 @@ public function testIsCallable(UnionType $type, TrinaryLogic $expectedResult): v $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isCallable()', $type->describe(VerbosityLevel::precise())), ); } - public function dataSelfCompare(): \Iterator + public function dataSelfCompare(): Iterator { - $broker = $this->createBroker(); + $reflectionProvider = $this->createReflectionProvider(); $integerType = new IntegerType(); $stringType = new StringType(); @@ -106,7 +116,7 @@ public function dataSelfCompare(): \Iterator yield [$constantStringType]; yield [new ErrorType()]; yield [new FloatType()]; - yield [new GenericClassStringType(new ObjectType(\Exception::class))]; + yield [new GenericClassStringType(new ObjectType(Exception::class))]; yield [new GenericObjectType('Foo', [new ObjectType('DateTime')])]; yield [new HasMethodType('Foo')]; yield [new HasOffsetType($constantStringType)]; @@ -123,14 +133,14 @@ public function dataSelfCompare(): \Iterator yield [new ObjectType('Foo')]; yield [new ObjectWithoutClassType(new ObjectType('Foo'))]; yield [new ResourceType()]; - yield [new StaticType('Foo')]; + yield [new StaticType($reflectionProvider->getClass('Foo'))]; yield [new StrictMixedType()]; yield [new StringAlwaysAcceptingObjectWithToStringType()]; yield [$stringType]; yield [TemplateTypeFactory::create($templateTypeScope, 'T', null, TemplateTypeVariance::createInvariant())]; yield [TemplateTypeFactory::create($templateTypeScope, 'T', new ObjectType('Foo'), TemplateTypeVariance::createInvariant())]; yield [TemplateTypeFactory::create($templateTypeScope, 'T', new ObjectWithoutClassType(), TemplateTypeVariance::createInvariant())]; - yield [new ThisType($broker->getClass('Foo'))]; + yield [new ThisType($reflectionProvider->getClass('Foo'))]; yield [new UnionType([$integerType, $stringType])]; yield [new VoidType()]; } @@ -138,28 +148,27 @@ public function dataSelfCompare(): \Iterator /** * @dataProvider dataSelfCompare * - * @param Type $type */ public function testSelfCompare(Type $type): void { $description = $type->describe(VerbosityLevel::precise()); $this->assertTrue( $type->equals($type), - sprintf('%s -> equals(itself)', $description) + sprintf('%s -> equals(itself)', $description), ); $this->assertEquals( 'Yes', $type->isSuperTypeOf($type)->describe(), - sprintf('%s -> isSuperTypeOf(itself)', $description) + sprintf('%s -> isSuperTypeOf(itself)', $description), ); $this->assertInstanceOf( get_class($type), TypeCombinator::union($type, $type), - sprintf('%s -> union with itself is same type', $description) + sprintf('%s -> union with itself is same type', $description), ); } - public function dataIsSuperTypeOf(): \Iterator + public function dataIsSuperTypeOf(): Iterator { $unionTypeA = new UnionType([ new IntegerType(), @@ -308,7 +317,7 @@ public function dataIsSuperTypeOf(): \Iterator yield [ $unionTypeB, - new ObjectType('Foo'), + new ObjectType(stdClass::class), TrinaryLogic::createNo(), ]; @@ -335,13 +344,115 @@ public function dataIsSuperTypeOf(): \Iterator new IntersectionType([new StringType(), new CallableType()]), TrinaryLogic::createNo(), ]; + + yield 'is super type of template-of-union with same members' => [ + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createYes(), + ]; + + yield 'is super type of template-of-union equal to a union member' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createYes(), + ]; + + yield 'maybe super type of template-of-union equal to a union member' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Bar'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createMaybe(), + ]; + + yield 'is super type of template-of-string equal to a union member' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createYes(), + ]; + + yield 'maybe super type of template-of-string sub type of a union member' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Bar'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createMaybe(), + ]; } /** * @dataProvider dataIsSuperTypeOf - * @param UnionType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSuperTypeOf(UnionType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -349,11 +460,11 @@ public function testIsSuperTypeOf(UnionType $type, Type $otherType, TrinaryLogic $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } - public function dataIsSubTypeOf(): \Iterator + public function dataIsSubTypeOf(): Iterator { $unionTypeA = new UnionType([ new IntegerType(), @@ -503,16 +614,13 @@ public function dataIsSubTypeOf(): \Iterator yield [ $unionTypeB, - new ObjectType('Foo'), + new ObjectType(stdClass::class), TrinaryLogic::createNo(), ]; } /** * @dataProvider dataIsSubTypeOf - * @param UnionType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSubTypeOf(UnionType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -520,15 +628,12 @@ public function testIsSubTypeOf(UnionType $type, Type $otherType, TrinaryLogic $ $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSubTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())) + sprintf('%s -> isSubTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())), ); } /** * @dataProvider dataIsSubTypeOf - * @param UnionType $type - * @param Type $otherType - * @param TrinaryLogic $expectedResult */ public function testIsSubTypeOfInversed(UnionType $type, Type $otherType, TrinaryLogic $expectedResult): void { @@ -536,7 +641,7 @@ public function testIsSubTypeOfInversed(UnionType $type, Type $otherType, Trinar $this->assertSame( $expectedResult->describe(), $actualResult->describe(), - sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())) + sprintf('%s -> isSuperTypeOf(%s)', $otherType->describe(VerbosityLevel::precise()), $type->describe(VerbosityLevel::precise())), ); } @@ -565,7 +670,7 @@ public function dataDescribe(): array new ConstantFloatType(2.2), new NullType(), new ConstantStringType('10'), - new ObjectType(\stdClass::class), + new ObjectType(stdClass::class), new ConstantBooleanType(true), new ConstantStringType('foo'), new ConstantStringType('2'), @@ -590,9 +695,9 @@ public function dataDescribe(): array new IntegerType(), new FloatType(), ]), - new ConstantStringType('aaa') + new ConstantStringType('aaa'), ), - '\'aaa\'|array(\'a\' => int|string, \'b\' => bool|float)', + '\'aaa\'|array{a: int|string, b: bool|float}', 'array|string', ], [ @@ -611,9 +716,9 @@ public function dataDescribe(): array new IntegerType(), new FloatType(), ]), - new ConstantStringType('aaa') + 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', ], [ @@ -632,9 +737,9 @@ public function dataDescribe(): array new IntegerType(), new FloatType(), ]), - new ConstantStringType('aaa') + 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', ], [ @@ -652,9 +757,9 @@ public function dataDescribe(): array new IntegerType(), new BooleanType(), new FloatType(), - ]) + ]), ), - 'array(0 => int|string, ?1 => bool, ?2 => float)', + 'array{0: int|string, 1?: bool, 2?: float}', 'array', ], [ @@ -664,9 +769,9 @@ public function dataDescribe(): array new ConstantStringType('foooo'), ], [ new ConstantStringType('barrr'), - ]) + ]), ), - 'array()|array(\'foooo\' => \'barrr\')', + 'array{}|array{foooo: \'barrr\'}', 'array', ], [ @@ -675,27 +780,32 @@ public function dataDescribe(): array new IntersectionType([ new StringType(), new AccessoryNumericStringType(), - ]) + ]), ), - 'int|(string&numeric)', + 'int|numeric-string', 'int|string', ], + [ + TypeCombinator::union( + IntegerRangeType::fromInterval(0, 4), + IntegerRangeType::fromInterval(6, 10), + ), + 'int<0, 4>|int<6, 10>', + 'int<0, 4>|int<6, 10>', + ], ]; } /** * @dataProvider dataDescribe - * @param Type $type - * @param string $expectedValueDescription - * @param string $expectedTypeOnlyDescription */ public function testDescribe( Type $type, string $expectedValueDescription, - string $expectedTypeOnlyDescription + string $expectedTypeOnlyDescription, ): void { - $this->assertSame($expectedValueDescription, $type->describe(VerbosityLevel::precise())); + $this->assertSame($expectedValueDescription, $type->describe(VerbosityLevel::value())); $this->assertSame($expectedTypeOnlyDescription, $type->describe(VerbosityLevel::typeOnly())); } @@ -769,25 +879,208 @@ public function dataAccepts(): array new ClosureType([], new MixedType(), false), TrinaryLogic::createYes(), ], + 'accepts template-of-union with same members' => [ + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createYes(), + ], + 'accepts template-of-union equal to a union member' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createYes(), + ], + 'accepts template-of-union sub type of a union member' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Bar'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createYes(), + ], + 'maybe accepts template-of-union sub type of a union member (argument)' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + )->toArgument(), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Bar'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createMaybe(), + ], + 'accepts template-of-string equal to a union member' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createYes(), + ], + 'accepts template-of-string sub type of a union member' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Bar'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createMaybe(), + ], + 'maybe accepts template-of-string sub type of a union member (argument)' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + )->toArgument(), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Bar'), + 'T', + new StringType(), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createMaybe(), + ], + 'accepts template-of-union containing a union member' => [ + new UnionType([ + new IntegerType(), + new NullType(), + ]), + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new IntegerType(), + new FloatType(), + ]), + TemplateTypeVariance::createInvariant(), + ), + TrinaryLogic::createMaybe(), + ], + 'accepts intersection with template-of-union equal to a union member' => [ + new UnionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new ObjectType('Iterator'), + new ObjectType('IteratorAggregate'), + ]), + TemplateTypeVariance::createInvariant(), + ), + new NullType(), + ]), + new IntersectionType([ + TemplateTypeFactory::create( + TemplateTypeScope::createWithClass('Foo'), + 'T', + new UnionType([ + new ObjectType('Iterator'), + new ObjectType('IteratorAggregate'), + ]), + TemplateTypeVariance::createInvariant(), + ), + new ObjectType('Countable'), + ]), + TrinaryLogic::createYes(), + ], + ]; } /** * @dataProvider dataAccepts - * @param UnionType $type - * @param Type $acceptedType - * @param TrinaryLogic $expectedResult */ public function testAccepts( UnionType $type, Type $acceptedType, - TrinaryLogic $expectedResult + TrinaryLogic $expectedResult, ): void { $this->assertSame( $expectedResult->describe(), $type->accepts($acceptedType, true)->describe(), - sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())) + sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $acceptedType->describe(VerbosityLevel::precise())), ); } @@ -795,12 +1088,12 @@ public function dataHasMethod(): array { return [ [ - new UnionType([new ObjectType(\DateTimeImmutable::class), new IntegerType()]), + new UnionType([new ObjectType(DateTimeImmutable::class), new IntegerType()]), 'format', TrinaryLogic::createMaybe(), ], [ - new UnionType([new ObjectType(\DateTimeImmutable::class), new ObjectType(\DateTime::class)]), + new UnionType([new ObjectType(DateTimeImmutable::class), new ObjectType(DateTime::class)]), 'format', TrinaryLogic::createYes(), ], @@ -810,7 +1103,7 @@ public function dataHasMethod(): array TrinaryLogic::createNo(), ], [ - new UnionType([new ObjectType(\DateTimeImmutable::class), new NullType()]), + new UnionType([new ObjectType(DateTimeImmutable::class), new NullType()]), 'format', TrinaryLogic::createMaybe(), ], @@ -819,14 +1112,11 @@ public function dataHasMethod(): array /** * @dataProvider dataHasMethod - * @param UnionType $type - * @param string $methodName - * @param TrinaryLogic $expectedResult */ public function testHasMethod( UnionType $type, string $methodName, - TrinaryLogic $expectedResult + TrinaryLogic $expectedResult, ): void { $this->assertSame($expectedResult->describe(), $type->hasMethod($methodName)->describe()); @@ -860,9 +1150,15 @@ public function testSorting(): void $type1 = new UnionType($types); $type2 = new UnionType(array_reverse($types)); + $this->assertSame( + implode("\n", array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::precise()), $type1->getTypes())), + implode("\n", array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::precise()), $type2->getTypes())), + 'UnionType sorting always produces the same order', + ); + $this->assertTrue( $type1->equals($type2), - 'UnionType sorting always produces the same order' + 'UnionType sorting always produces the same order', ); } diff --git a/tests/PHPStan/Type/data/cyclic-phpdocs.php b/tests/PHPStan/Type/data/cyclic-phpdocs.php index f305af38bb..b22c22980f 100644 --- a/tests/PHPStan/Type/data/cyclic-phpdocs.php +++ b/tests/PHPStan/Type/data/cyclic-phpdocs.php @@ -5,5 +5,6 @@ interface Foo extends \IteratorAggregate { /** @return iterable | Foo */ + #[\ReturnTypeWillChange] public function getIterator(); } diff --git a/tests/PHPStan/Type/data/dependent-phpdocs.php b/tests/PHPStan/Type/data/dependent-phpdocs.php index df181a3608..66deecea49 100644 --- a/tests/PHPStan/Type/data/dependent-phpdocs.php +++ b/tests/PHPStan/Type/data/dependent-phpdocs.php @@ -8,5 +8,5 @@ interface Foo extends \IteratorAggregate public function addPages($pages); /** non-empty */ - public function getIterator(); + public function getIterator(): \Traversable; } diff --git a/tests/bootstrap-runtime-reflection.php b/tests/bootstrap-runtime-reflection.php index 7598e4fa5f..2fb1c54480 100644 --- a/tests/bootstrap-runtime-reflection.php +++ b/tests/bootstrap-runtime-reflection.php @@ -1,5 +1,7 @@ assertSame('Parameter #1 $source of function token_get_all expects string, PhpParser\Node\Expr\MethodCall given.', $result['files'][$fileHelper->normalizePath(__DIR__ . '/PHP-Parser/lib/PhpParser/Lexer.php')]['messages'][0]['message']); + $this->assertSame('Parameter #1 $code of function token_get_all expects string, PhpParser\Node\Expr\MethodCall given.', $result['files'][$fileHelper->normalizePath(__DIR__ . '/PHP-Parser/lib/PhpParser/Lexer.php')]['messages'][0]['message']); $this->assertSame('Parameter #1 $code of method PhpParser\Lexer::startLexing() expects PhpParser\Node\Expr\MethodCall, string given.', $result['files'][$fileHelper->normalizePath(__DIR__ . '/PHP-Parser/lib/PhpParser/ParserAbstract.php')]['messages'][0]['message']); - $this->assertSame('Parameter #1 (array(\'foo\')) of echo cannot be converted to string.', $result['files'][$fileHelper->normalizePath(__DIR__ . '/PHP-Parser/lib/bootstrap.php')]['messages'][0]['message']); + $this->assertSame('Parameter #1 (array{\'foo\'}) of echo cannot be converted to string.', $result['files'][$fileHelper->normalizePath(__DIR__ . '/PHP-Parser/lib/bootstrap.php')]['messages'][0]['message']); $this->assertResultCache(__DIR__ . '/resultCache_2.php'); } @@ -104,7 +116,7 @@ public function testResultCacheDeleteFile(): void $fileHelper = new FileHelper(__DIR__); $result = $this->runPhpstan(1); - $this->assertSame(4, $result['totals']['file_errors'], Json::encode($result)); + $this->assertSame(5, $result['totals']['file_errors'], Json::encode($result)); $this->assertSame(0, $result['totals']['errors'], Json::encode($result)); $message = $result['files'][$fileHelper->normalizePath(__DIR__ . '/PHP-Parser/lib/PhpParser/Serializer/XML.php')]['messages'][0]['message']; @@ -113,6 +125,7 @@ public function testResultCacheDeleteFile(): void $this->assertSame('Reflection error: PhpParser\Serializer not found.', $result['files'][$fileHelper->normalizePath(__DIR__ . '/PHP-Parser/lib/PhpParser/Serializer/XML.php')]['messages'][1]['message']); $this->assertSame('Reflection error: PhpParser\Serializer not found.', $result['files'][$fileHelper->normalizePath(__DIR__ . '/PHP-Parser/lib/PhpParser/Serializer/XML.php')]['messages'][2]['message']); $this->assertSame('Reflection error: PhpParser\Serializer not found.', $result['files'][$fileHelper->normalizePath(__DIR__ . '/PHP-Parser/lib/PhpParser/Serializer/XML.php')]['messages'][3]['message']); + $this->assertSame('Reflection error: PhpParser\Serializer not found.', $result['files'][$fileHelper->normalizePath(__DIR__ . '/PHP-Parser/lib/PhpParser/Serializer/XML.php')]['messages'][4]['message']); file_put_contents($serializerPath, $originalSerializerCode); $this->runPhpstan(0); @@ -120,7 +133,6 @@ public function testResultCacheDeleteFile(): void } /** - * @param int $expectedExitCode * @return mixed[] */ private function runPhpstan(int $expectedExitCode): array @@ -129,13 +141,13 @@ private function runPhpstan(int $expectedExitCode): array '%s %s analyse -c %s -l 5 --no-progress --error-format json lib 2>&1', escapeshellarg(PHP_BINARY), escapeshellarg(__DIR__ . '/../../bin/phpstan'), - escapeshellarg(__DIR__ . '/phpstan.neon') + escapeshellarg(__DIR__ . '/phpstan.neon'), ), $outputLines, $exitCode); $output = implode("\n", $outputLines); try { $json = Json::decode($output, Json::FORCE_ARRAY); - } catch (\Nette\Utils\JsonException $e) { + } catch (JsonException $e) { $this->fail(sprintf('%s: %s', $e->getMessage(), $output)); } @@ -154,9 +166,7 @@ private function transformResultCache(array $resultCache): array { $new = []; foreach ($resultCache['dependencies'] as $file => $data) { - $files = array_map(function (string $file): string { - return $this->relativizePath($file); - }, $data['dependentFiles']); + $files = array_map(fn (string $file): string => $this->relativizePath($file), $data['dependentFiles']); sort($files); $new[$this->relativizePath($file)] = $files; } diff --git a/tests/phpstan-bootstrap.php b/tests/phpstan-bootstrap.php index 8360247df4..d79849b085 100644 --- a/tests/phpstan-bootstrap.php +++ b/tests/phpstan-bootstrap.php @@ -1,4 +1,9 @@